mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:27:19 +00:00
Add functional marketplace modules, bottom nav bar, root terminal, arcade games
- ModuleManager: Central tracking for installed marketplace modules - DataAnalyzerWidget: Real-time CPU/RAM/Battery/Storage widget (unlocked by Data Analyzer module) - BottomNavBar: Navigation bar for Projects/Chat/Marketplace/Settings - RootShell: Real root command execution utility - TerminalActivity: Full root shell with neofetch, sysinfo, real Linux commands - Terminal Pro module: Adds aliases (ll, la, h), command history - ArcadeActivity + SnakeGame: Pixel Arcade module unlocks retro games - fade_in/fade_out animations for smooth transitions
This commit is contained in:
parent
b04b8f8dca
commit
b3c308b2c8
1842 changed files with 328426 additions and 717 deletions
36
android/.claude/settings.local.json
Normal file
36
android/.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(python3:*)",
|
||||||
|
"Bash(gradlew.bat assembleDebug:*)",
|
||||||
|
"Bash(cmd /c \"gradlew.bat assembleDebug\")",
|
||||||
|
"Bash(cmd.exe /c \"gradlew.bat assembleDebug 2>&1\")",
|
||||||
|
"Bash(./gradlew assembleDebug:*)",
|
||||||
|
"Bash(.\\\\gradlew assembleDebug:*)",
|
||||||
|
"Bash(.\\\\gradlew.bat assembleDebug:*)",
|
||||||
|
"Bash(cmd /c:*)",
|
||||||
|
"Bash(powershell -Command:*)",
|
||||||
|
"Bash(Select-String -Pattern \"HOME\" -Context 1,3)",
|
||||||
|
"Bash(Select-String -Pattern \"HOME\" -Context 0,5)",
|
||||||
|
"Bash(powershell -ExecutionPolicy Bypass -File:*)",
|
||||||
|
"Bash(node:*)",
|
||||||
|
"Bash(ren:*)",
|
||||||
|
"Bash(adb push:*)",
|
||||||
|
"WebFetch(domain:xdaforums.com)",
|
||||||
|
"WebFetch(domain:topjohnwu.github.io)",
|
||||||
|
"WebFetch(domain:www.needrom.com)",
|
||||||
|
"WebFetch(domain:firmwaredrive.com)",
|
||||||
|
"WebFetch(domain:phonefirmware.com)",
|
||||||
|
"Bash(adb devices:*)",
|
||||||
|
"Bash(adb shell getprop:*)",
|
||||||
|
"Bash(adb shell:*)",
|
||||||
|
"Bash(where:*)",
|
||||||
|
"Bash(C:\\\\Users\\\\PCOEM\\\\platform-tools\\\\fastboot.exe reboot:*)",
|
||||||
|
"Bash(adb kill-server:*)",
|
||||||
|
"Bash(adb start-server:*)",
|
||||||
|
"Bash(powershell:*)",
|
||||||
|
"Bash(adb connect:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1
android/.claude/worktrees/distracted-napier
Submodule
1
android/.claude/worktrees/distracted-napier
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit b04b8f8dca4364d3359e405fdda40cc5d453f564
|
||||||
13
android/.idea/deviceManager.xml
Normal file
13
android/.idea/deviceManager.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.aethex.os"
|
namespace "com.aethex.os"
|
||||||
compileSdk = rootProject.ext.compileSdkVersion
|
compileSdk 34
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.aethex.os"
|
applicationId "com.aethex.os"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdk 24
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdk 34
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
|
||||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
|
||||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
|
@ -24,31 +19,12 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
flatDir{
|
|
||||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||||
implementation project(':capacitor-android')
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation "junit:junit:$junitVersion"
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
||||||
implementation project(':capacitor-cordova-android-plugins')
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: 'capacitor.build.gradle'
|
|
||||||
|
|
||||||
try {
|
|
||||||
def servicesJSON = file('google-services.json')
|
|
||||||
if (servicesJSON.text) {
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,155 @@
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/title_activity_main"
|
android:exported="true"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true">
|
android:screenOrientation="portrait"
|
||||||
|
android:stateNotNeeded="true"
|
||||||
|
android:excludeFromRecents="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Register as a home launcher so user can set AeThexOS as default -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.HOME" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".SystemActivity"
|
||||||
|
android:label="AeThex System"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:excludeFromRecents="true" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AppActivity"
|
||||||
|
android:label="App"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".SettingsActivity"
|
||||||
|
android:label="Settings"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".CalculatorActivity"
|
||||||
|
android:label="Calculator"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".TerminalActivity"
|
||||||
|
android:label="Terminal"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ClockActivity"
|
||||||
|
android:label="Clock"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".SnakeActivity"
|
||||||
|
android:label="Snake"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".NotesActivity"
|
||||||
|
android:label="Notes"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".WeatherActivity"
|
||||||
|
android:label="Weather"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MinesweeperActivity"
|
||||||
|
android:label="Minesweeper"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".FileManagerActivity"
|
||||||
|
android:label="File Manager"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MusicActivity"
|
||||||
|
android:label="Music"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ChatActivity"
|
||||||
|
android:label="Messages"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".PhotosActivity"
|
||||||
|
android:label="Photos"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".BrowserActivity"
|
||||||
|
android:label="Browser"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".PassportActivity"
|
||||||
|
android:label="Passport"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ProjectsActivity"
|
||||||
|
android:label="Projects"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MarketplaceActivity"
|
||||||
|
android:label="Marketplace"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ArcadeActivity"
|
||||||
|
android:label="Arcade"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AnalyticsActivity"
|
||||||
|
android:label="Analytics"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MailActivity"
|
||||||
|
android:label="Mail"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".CameraActivity"
|
||||||
|
android:label="Camera"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AchievementsActivity"
|
||||||
|
android:label="Achievements"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<!-- Notification Listener for real Android notifications -->
|
||||||
|
<service
|
||||||
|
android:name=".AeThexNotificationService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.notification.NotificationListenerService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
|
@ -32,18 +167,21 @@
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths"></meta-data>
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.camera.front" android:required="false" />
|
<uses-feature android:name="android.hardware.camera.front" android:required="false" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
public class AchievementsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private Typeface monoFont;
|
||||||
|
private Typeface displayFont;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "aethex_achievements";
|
||||||
|
|
||||||
|
// {id, icon, title, description, condition_key}
|
||||||
|
private static final String[][] ACHIEVEMENTS = {
|
||||||
|
{"first_boot", "⚡", "First Boot", "Boot into AeThex OS for the first time", "true"},
|
||||||
|
{"explorer", "📁", "Explorer", "Open the File Manager", "files"},
|
||||||
|
{"codebreaker", "💻", "Codebreaker", "Execute 5 commands in Terminal", "terminal_cmds"},
|
||||||
|
{"snake_master", "🐍", "Snake Master", "Score 50+ points in Snake", "snake_score"},
|
||||||
|
{"minesweeper_win", "💣", "Bomb Defuser", "Win a game of Minesweeper", "minesweeper_win"},
|
||||||
|
{"note_taker", "📝", "Note Taker", "Create 3 notes in the Notes app", "notes_count"},
|
||||||
|
{"clearance_swap", "🔄", "Identity Crisis", "Switch clearance mode 3 times", "clearance_swaps"},
|
||||||
|
{"music_lover", "🎵", "Music Lover", "Play 5 tracks in the Radio app", "tracks_played"},
|
||||||
|
{"calculator_pro", "🔢", "Number Cruncher", "Perform 20 calculations", "calc_ops"},
|
||||||
|
{"photographer", "📷", "Photographer", "Take 5 photos in the Camera app", "photos_taken"},
|
||||||
|
{"messenger", "💬", "Social Butterfly", "Send 10 messages in Chat", "messages_sent"},
|
||||||
|
{"weatherman", "🌤", "Weatherman", "Check the weather 5 times", "weather_checks"},
|
||||||
|
{"time_keeper", "⏱", "Time Keeper", "Use the stopwatch for 60 seconds", "stopwatch_time"},
|
||||||
|
{"browser_surfer", "🌐", "Web Surfer", "Visit 5 URLs in the Browser", "urls_visited"},
|
||||||
|
{"konami", "🎮", "Konami Master", "Enter the Konami code in Settings", "konami_unlocked"},
|
||||||
|
{"marathon", "🏃", "Marathon", "Keep AeThex OS running for 30 minutes", "uptime_30"},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
monoFont = themeManager.getMonoFont(this);
|
||||||
|
displayFont = themeManager.getDisplayFont(this);
|
||||||
|
prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
// Mark first boot achievement
|
||||||
|
if (!prefs.getBoolean("first_boot", false)) {
|
||||||
|
prefs.edit().putBoolean("first_boot", true).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView title = findViewById(R.id.app_title);
|
||||||
|
title.setText("Achievements");
|
||||||
|
TextView nameDisplay = findViewById(R.id.app_name_display);
|
||||||
|
|
||||||
|
findViewById(R.id.app_back).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout content = (LinearLayout) nameDisplay.getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
content.setGravity(Gravity.TOP);
|
||||||
|
content.setPadding(0, 0, 0, 0);
|
||||||
|
buildAchievementsUI(content);
|
||||||
|
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildAchievementsUI(LinearLayout parent) {
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
|
||||||
|
// Stats header
|
||||||
|
LinearLayout statsBar = new LinearLayout(this);
|
||||||
|
statsBar.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
statsBar.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
statsBar.setPadding(dpToPx(16), dpToPx(14), dpToPx(16), dpToPx(14));
|
||||||
|
|
||||||
|
int unlockedCount = countUnlocked();
|
||||||
|
|
||||||
|
TextView statsLabel = new TextView(this);
|
||||||
|
statsLabel.setText("ACHIEVEMENTS");
|
||||||
|
statsLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
statsLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
statsLabel.setTypeface(monoFont);
|
||||||
|
statsLabel.setLetterSpacing(0.15f);
|
||||||
|
statsBar.addView(statsLabel);
|
||||||
|
|
||||||
|
View sp = new View(this);
|
||||||
|
sp.setLayoutParams(new LinearLayout.LayoutParams(0, 1, 1f));
|
||||||
|
statsBar.addView(sp);
|
||||||
|
|
||||||
|
// Progress text
|
||||||
|
TextView progress = new TextView(this);
|
||||||
|
progress.setText(unlockedCount + " / " + ACHIEVEMENTS.length);
|
||||||
|
progress.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
progress.setTextColor(primaryColor);
|
||||||
|
progress.setTypeface(displayFont);
|
||||||
|
statsBar.addView(progress);
|
||||||
|
|
||||||
|
parent.addView(statsBar);
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
FrameLayout progressBarFrame = new FrameLayout(this);
|
||||||
|
LinearLayout.LayoutParams pbfP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(4));
|
||||||
|
pbfP.setMarginStart(dpToPx(16));
|
||||||
|
pbfP.setMarginEnd(dpToPx(16));
|
||||||
|
pbfP.bottomMargin = dpToPx(4);
|
||||||
|
progressBarFrame.setLayoutParams(pbfP);
|
||||||
|
|
||||||
|
// Track
|
||||||
|
View track = new View(this);
|
||||||
|
track.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
GradientDrawable trackBg = new GradientDrawable();
|
||||||
|
trackBg.setCornerRadius(dpToPx(2));
|
||||||
|
trackBg.setColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
track.setBackground(trackBg);
|
||||||
|
progressBarFrame.addView(track);
|
||||||
|
|
||||||
|
// Fill
|
||||||
|
float pct = (float) unlockedCount / ACHIEVEMENTS.length;
|
||||||
|
View fill = new View(this);
|
||||||
|
fill.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
GradientDrawable fillBg = new GradientDrawable();
|
||||||
|
fillBg.setCornerRadius(dpToPx(2));
|
||||||
|
fillBg.setColor(primaryColor);
|
||||||
|
fill.setBackground(fillBg);
|
||||||
|
progressBarFrame.addView(fill);
|
||||||
|
|
||||||
|
// Measure after layout
|
||||||
|
progressBarFrame.post(() -> {
|
||||||
|
int totalWidth = progressBarFrame.getWidth();
|
||||||
|
int fillWidth = (int) (totalWidth * pct);
|
||||||
|
fill.getLayoutParams().width = fillWidth;
|
||||||
|
fill.requestLayout();
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.addView(progressBarFrame);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
View div = new View(this);
|
||||||
|
LinearLayout.LayoutParams divP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(1));
|
||||||
|
divP.topMargin = dpToPx(8);
|
||||||
|
div.setLayoutParams(divP);
|
||||||
|
div.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
parent.addView(div);
|
||||||
|
|
||||||
|
// Achievement list
|
||||||
|
ScrollView scroll = new ScrollView(this);
|
||||||
|
scroll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
scroll.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
|
||||||
|
LinearLayout list = new LinearLayout(this);
|
||||||
|
list.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
list.setPadding(dpToPx(12), dpToPx(8), dpToPx(12), dpToPx(12));
|
||||||
|
|
||||||
|
for (String[] ach : ACHIEVEMENTS) {
|
||||||
|
boolean unlocked = isUnlocked(ach[0]);
|
||||||
|
list.addView(buildAchievementCard(ach, unlocked, primaryColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll.addView(list);
|
||||||
|
parent.addView(scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View buildAchievementCard(String[] ach, boolean unlocked, int primaryColor) {
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
card.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
card.setPadding(dpToPx(14), dpToPx(12), dpToPx(14), dpToPx(12));
|
||||||
|
LinearLayout.LayoutParams cardP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
cardP.bottomMargin = dpToPx(6);
|
||||||
|
card.setLayoutParams(cardP);
|
||||||
|
|
||||||
|
GradientDrawable cardBg = new GradientDrawable();
|
||||||
|
cardBg.setCornerRadius(dpToPx(10));
|
||||||
|
if (unlocked) {
|
||||||
|
cardBg.setColor(Color.argb(26, Color.red(primaryColor), Color.green(primaryColor), Color.blue(primaryColor)));
|
||||||
|
cardBg.setStroke(dpToPx(1), Color.argb(51, Color.red(primaryColor), Color.green(primaryColor), Color.blue(primaryColor)));
|
||||||
|
} else {
|
||||||
|
cardBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
cardBg.setStroke(dpToPx(1), Color.parseColor("#0DFFFFFF"));
|
||||||
|
}
|
||||||
|
card.setBackground(cardBg);
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
TextView icon = new TextView(this);
|
||||||
|
icon.setText(ach[1]);
|
||||||
|
icon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
|
||||||
|
icon.setAlpha(unlocked ? 1f : 0.3f);
|
||||||
|
icon.setGravity(Gravity.CENTER);
|
||||||
|
icon.setLayoutParams(new LinearLayout.LayoutParams(dpToPx(40), dpToPx(40)));
|
||||||
|
card.addView(icon);
|
||||||
|
|
||||||
|
// Text column
|
||||||
|
LinearLayout textCol = new LinearLayout(this);
|
||||||
|
textCol.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
LinearLayout.LayoutParams tcP = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
tcP.setMarginStart(dpToPx(12));
|
||||||
|
textCol.setLayoutParams(tcP);
|
||||||
|
|
||||||
|
TextView titleTv = new TextView(this);
|
||||||
|
titleTv.setText(ach[2]);
|
||||||
|
titleTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
titleTv.setTextColor(unlocked ? Color.WHITE : Color.parseColor("#66FFFFFF"));
|
||||||
|
titleTv.setTypeface(displayFont);
|
||||||
|
textCol.addView(titleTv);
|
||||||
|
|
||||||
|
TextView descTv = new TextView(this);
|
||||||
|
descTv.setText(ach[3]);
|
||||||
|
descTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
descTv.setTextColor(unlocked ? Color.parseColor("#99FFFFFF") : Color.parseColor("#33FFFFFF"));
|
||||||
|
descTv.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams dP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
dP.topMargin = dpToPx(2);
|
||||||
|
descTv.setLayoutParams(dP);
|
||||||
|
textCol.addView(descTv);
|
||||||
|
|
||||||
|
card.addView(textCol);
|
||||||
|
|
||||||
|
// Status indicator
|
||||||
|
if (unlocked) {
|
||||||
|
TextView check = new TextView(this);
|
||||||
|
check.setText("✓");
|
||||||
|
check.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||||
|
check.setTextColor(primaryColor);
|
||||||
|
check.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
card.addView(check);
|
||||||
|
} else {
|
||||||
|
// Locked icon
|
||||||
|
TextView lock = new TextView(this);
|
||||||
|
lock.setText("🔒");
|
||||||
|
lock.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
lock.setAlpha(0.3f);
|
||||||
|
card.addView(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUnlocked(String id) {
|
||||||
|
switch (id) {
|
||||||
|
case "first_boot":
|
||||||
|
return true; // Always unlocked
|
||||||
|
case "konami":
|
||||||
|
return new ThemeManager(this).isKonamiUnlocked();
|
||||||
|
case "clearance_swap":
|
||||||
|
return prefs.getInt("clearance_swaps", 0) >= 3;
|
||||||
|
case "codebreaker":
|
||||||
|
return prefs.getInt("terminal_cmds", 0) >= 5;
|
||||||
|
case "snake_master":
|
||||||
|
return prefs.getInt("snake_score", 0) >= 50;
|
||||||
|
case "minesweeper_win":
|
||||||
|
return prefs.getBoolean("minesweeper_win", false);
|
||||||
|
case "note_taker":
|
||||||
|
return prefs.getInt("notes_count", 0) >= 3;
|
||||||
|
case "music_lover":
|
||||||
|
return prefs.getInt("tracks_played", 0) >= 5;
|
||||||
|
case "calculator_pro":
|
||||||
|
return prefs.getInt("calc_ops", 0) >= 20;
|
||||||
|
case "photographer":
|
||||||
|
return prefs.getInt("photos_taken", 0) >= 5;
|
||||||
|
case "messenger":
|
||||||
|
return prefs.getInt("messages_sent", 0) >= 10;
|
||||||
|
case "weatherman":
|
||||||
|
return prefs.getInt("weather_checks", 0) >= 5;
|
||||||
|
case "time_keeper":
|
||||||
|
return prefs.getInt("stopwatch_time", 0) >= 60;
|
||||||
|
case "browser_surfer":
|
||||||
|
return prefs.getInt("urls_visited", 0) >= 5;
|
||||||
|
case "explorer":
|
||||||
|
return prefs.getBoolean("files_opened", false);
|
||||||
|
case "marathon":
|
||||||
|
return prefs.getBoolean("uptime_30", false);
|
||||||
|
default:
|
||||||
|
return prefs.getBoolean(id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countUnlocked() {
|
||||||
|
int count = 0;
|
||||||
|
for (String[] ach : ACHIEVEMENTS) {
|
||||||
|
if (isUnlocked(ach[0])) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void incrementStat(android.content.Context ctx, String key) {
|
||||||
|
SharedPreferences p = ctx.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
p.edit().putInt(key, p.getInt(key, 0) + 1).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setBoolStat(android.content.Context ctx, String key) {
|
||||||
|
SharedPreferences p = ctx.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
p.edit().putBoolean(key, true).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
191
android/app/src/main/java/com/aethex/os/AeThexContextMenu.java
Normal file
191
android/app/src/main/java/com/aethex/os/AeThexContextMenu.java
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context menu overlay for long-press on app icons and desktop.
|
||||||
|
* Shows a floating menu with options like "Open", "Info", etc.
|
||||||
|
*/
|
||||||
|
public class AeThexContextMenu {
|
||||||
|
|
||||||
|
public interface MenuAction {
|
||||||
|
void onAction(String actionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String OVERLAY_TAG = "aethex_context_menu";
|
||||||
|
|
||||||
|
public static class MenuItem {
|
||||||
|
public final String id;
|
||||||
|
public final String label;
|
||||||
|
public final String iconChar; // emoji or unicode
|
||||||
|
|
||||||
|
public MenuItem(String id, String label, String iconChar) {
|
||||||
|
this.id = id;
|
||||||
|
this.label = label;
|
||||||
|
this.iconChar = iconChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a context menu at the given coordinates.
|
||||||
|
*/
|
||||||
|
public static void show(Activity activity, float x, float y,
|
||||||
|
String title, MenuItem[] items, MenuAction action) {
|
||||||
|
if (activity == null || activity.isFinishing() || activity.isDestroyed()) return;
|
||||||
|
|
||||||
|
dismiss(activity); // Remove any existing
|
||||||
|
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
|
||||||
|
// Scrim (semi-transparent background that dismisses on tap)
|
||||||
|
FrameLayout scrim = new FrameLayout(activity);
|
||||||
|
scrim.setTag(OVERLAY_TAG);
|
||||||
|
scrim.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
scrim.setBackgroundColor(Color.parseColor("#40000000"));
|
||||||
|
scrim.setOnClickListener(v -> dismiss(activity));
|
||||||
|
|
||||||
|
// Menu card
|
||||||
|
LinearLayout menu = new LinearLayout(activity);
|
||||||
|
menu.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
int menuWidth = dpToPx(activity, 180);
|
||||||
|
FrameLayout.LayoutParams menuParams = new FrameLayout.LayoutParams(
|
||||||
|
menuWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
|
||||||
|
// Position the menu near the tap point, clamping to screen
|
||||||
|
int screenW = decorView.getWidth();
|
||||||
|
int screenH = decorView.getHeight();
|
||||||
|
int menuX = (int) Math.min(x, screenW - menuWidth - dpToPx(activity, 16));
|
||||||
|
int menuY = (int) Math.min(y, screenH - dpToPx(activity, 200));
|
||||||
|
menuX = Math.max(menuX, dpToPx(activity, 8));
|
||||||
|
menuY = Math.max(menuY, dpToPx(activity, 8));
|
||||||
|
|
||||||
|
menuParams.leftMargin = menuX;
|
||||||
|
menuParams.topMargin = menuY;
|
||||||
|
menu.setLayoutParams(menuParams);
|
||||||
|
|
||||||
|
GradientDrawable menuBg = new GradientDrawable();
|
||||||
|
menuBg.setCornerRadius(dpToPx(activity, 12));
|
||||||
|
menuBg.setColor(Color.parseColor("#E6111827"));
|
||||||
|
menuBg.setStroke(dpToPx(activity, 1), Color.parseColor("#33FFFFFF"));
|
||||||
|
menu.setBackground(menuBg);
|
||||||
|
menu.setElevation(dpToPx(activity, 8));
|
||||||
|
menu.setClipToOutline(true);
|
||||||
|
menu.setPadding(0, dpToPx(activity, 6), 0, dpToPx(activity, 6));
|
||||||
|
|
||||||
|
Typeface monoFont;
|
||||||
|
try {
|
||||||
|
monoFont = ResourcesCompat.getFont(activity, R.font.source_code_pro);
|
||||||
|
} catch (Exception e) {
|
||||||
|
monoFont = Typeface.MONOSPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title header
|
||||||
|
if (title != null) {
|
||||||
|
TextView titleView = new TextView(activity);
|
||||||
|
titleView.setText(title);
|
||||||
|
titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
titleView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
titleView.setTypeface(monoFont);
|
||||||
|
titleView.setLetterSpacing(0.1f);
|
||||||
|
titleView.setPadding(dpToPx(activity, 14), dpToPx(activity, 6),
|
||||||
|
dpToPx(activity, 14), dpToPx(activity, 6));
|
||||||
|
menu.addView(titleView);
|
||||||
|
|
||||||
|
View div = new View(activity);
|
||||||
|
div.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(activity, 1)));
|
||||||
|
div.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
LinearLayout.LayoutParams divParams = (LinearLayout.LayoutParams) div.getLayoutParams();
|
||||||
|
divParams.bottomMargin = dpToPx(activity, 2);
|
||||||
|
divParams.topMargin = dpToPx(activity, 2);
|
||||||
|
menu.addView(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu items
|
||||||
|
Typeface finalMonoFont = monoFont;
|
||||||
|
for (MenuItem item : items) {
|
||||||
|
LinearLayout row = new LinearLayout(activity);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
row.setPadding(dpToPx(activity, 14), dpToPx(activity, 10),
|
||||||
|
dpToPx(activity, 14), dpToPx(activity, 10));
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
if (item.iconChar != null) {
|
||||||
|
TextView icon = new TextView(activity);
|
||||||
|
icon.setText(item.iconChar);
|
||||||
|
icon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
||||||
|
dpToPx(activity, 24), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
icon.setLayoutParams(iconParams);
|
||||||
|
icon.setGravity(Gravity.CENTER);
|
||||||
|
row.addView(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
TextView label = new TextView(activity);
|
||||||
|
label.setText(item.label);
|
||||||
|
label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
label.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
label.setTypeface(finalMonoFont);
|
||||||
|
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
labelParams.setMarginStart(dpToPx(activity, 8));
|
||||||
|
label.setLayoutParams(labelParams);
|
||||||
|
row.addView(label);
|
||||||
|
|
||||||
|
row.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
dismiss(activity);
|
||||||
|
if (action != null) {
|
||||||
|
action.onAction(item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.addView(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrim.addView(menu);
|
||||||
|
|
||||||
|
// Animate in
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
menu.setScaleX(0.8f);
|
||||||
|
menu.setScaleY(0.8f);
|
||||||
|
|
||||||
|
decorView.addView(scrim);
|
||||||
|
|
||||||
|
scrim.animate().alpha(1f).setDuration(150).start();
|
||||||
|
menu.animate().scaleX(1f).scaleY(1f).setDuration(150).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void dismiss(Activity activity) {
|
||||||
|
if (activity == null) return;
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
View existing = decorView.findViewWithTag(OVERLAY_TAG);
|
||||||
|
if (existing != null) {
|
||||||
|
existing.animate().alpha(0f).setDuration(100).withEndAction(() -> {
|
||||||
|
decorView.removeView(existing);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int dpToPx(Activity activity, float dp) {
|
||||||
|
return Math.round(TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, dp,
|
||||||
|
activity.getResources().getDisplayMetrics()));
|
||||||
|
}
|
||||||
|
}
|
||||||
503
android/app/src/main/java/com/aethex/os/AeThexKeyboard.java
Normal file
503
android/app/src/main/java/com/aethex/os/AeThexKeyboard.java
Normal file
|
|
@ -0,0 +1,503 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom in-app keyboard overlay that replaces the system soft keyboard.
|
||||||
|
* Styled to match the AeThex OS clearance theme (Foundation red/gold or Corp blue/silver).
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* AeThexKeyboard.attach(activity); // in onCreate, after setContentView
|
||||||
|
* AeThexKeyboard.detach(activity); // optional cleanup in onDestroy
|
||||||
|
*/
|
||||||
|
public class AeThexKeyboard {
|
||||||
|
|
||||||
|
private static final String KEYBOARD_TAG = "aethex_keyboard";
|
||||||
|
private static final String KEYBOARD_OVERLAY_TAG = "aethex_keyboard_overlay";
|
||||||
|
|
||||||
|
// Key layouts
|
||||||
|
private static final String[][] ALPHA_ROWS = {
|
||||||
|
{"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"},
|
||||||
|
{"a", "s", "d", "f", "g", "h", "j", "k", "l"},
|
||||||
|
{"⇧", "z", "x", "c", "v", "b", "n", "m", "⌫"},
|
||||||
|
{"?123", " ", ".", "↵"}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String[][] SYMBOL_ROWS = {
|
||||||
|
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"},
|
||||||
|
{"@", "#", "$", "%", "&", "-", "+", "(", ")"},
|
||||||
|
{"=", "*", "\"", "'", ":", ";", "!", "?", "⌫"},
|
||||||
|
{"ABC", " ", "/", "↵"}
|
||||||
|
};
|
||||||
|
|
||||||
|
// State
|
||||||
|
private static boolean showingSymbols = false;
|
||||||
|
private static boolean shiftActive = false;
|
||||||
|
private static EditText currentEditText = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach the custom keyboard to an Activity.
|
||||||
|
* Suppresses the system keyboard and shows the themed AeThex keyboard instead.
|
||||||
|
*/
|
||||||
|
public static void attach(Activity activity) {
|
||||||
|
if (activity == null) return;
|
||||||
|
|
||||||
|
// Prevent the system keyboard from showing automatically
|
||||||
|
activity.getWindow().setSoftInputMode(
|
||||||
|
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||||
|
|
||||||
|
// Walk the view tree and find all EditTexts — hook into their focus
|
||||||
|
View rootView = activity.getWindow().getDecorView().getRootView();
|
||||||
|
hookEditTexts(activity, rootView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach to a specific EditText programmatically (for dynamic EditTexts like Spotlight).
|
||||||
|
*/
|
||||||
|
public static void attachToEditText(Activity activity, EditText editText) {
|
||||||
|
if (activity == null || editText == null) return;
|
||||||
|
|
||||||
|
editText.setShowSoftInputOnFocus(false);
|
||||||
|
|
||||||
|
editText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus) {
|
||||||
|
hideSystemKeyboard(activity, editText);
|
||||||
|
currentEditText = editText;
|
||||||
|
showKeyboard(activity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also handle click (EditText may already have focus when tapped)
|
||||||
|
editText.setOnClickListener(v -> {
|
||||||
|
hideSystemKeyboard(activity, editText);
|
||||||
|
currentEditText = editText;
|
||||||
|
if (!isKeyboardShowing(activity)) {
|
||||||
|
showKeyboard(activity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach and remove the keyboard overlay from an activity.
|
||||||
|
*/
|
||||||
|
public static void detach(Activity activity) {
|
||||||
|
dismissKeyboard(activity);
|
||||||
|
currentEditText = null;
|
||||||
|
showingSymbols = false;
|
||||||
|
shiftActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the keyboard if showing.
|
||||||
|
*/
|
||||||
|
public static void dismissKeyboard(Activity activity) {
|
||||||
|
if (activity == null) return;
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
View existing = decorView.findViewWithTag(KEYBOARD_OVERLAY_TAG);
|
||||||
|
if (existing != null) {
|
||||||
|
existing.animate().translationY(existing.getHeight()).alpha(0.5f)
|
||||||
|
.setDuration(150).withEndAction(() -> {
|
||||||
|
decorView.removeView(existing);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isKeyboardShowing(Activity activity) {
|
||||||
|
if (activity == null) return false;
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
return decorView.findViewWithTag(KEYBOARD_OVERLAY_TAG) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// Private: Hook all EditTexts
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void hookEditTexts(Activity activity, View view) {
|
||||||
|
if (view instanceof EditText) {
|
||||||
|
EditText editText = (EditText) view;
|
||||||
|
|
||||||
|
// Prevent system keyboard from appearing
|
||||||
|
editText.setShowSoftInputOnFocus(false);
|
||||||
|
|
||||||
|
// Save original focus listener if any
|
||||||
|
editText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus) {
|
||||||
|
hideSystemKeyboard(activity, editText);
|
||||||
|
currentEditText = editText;
|
||||||
|
showKeyboard(activity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editText.setOnClickListener(v -> {
|
||||||
|
hideSystemKeyboard(activity, editText);
|
||||||
|
currentEditText = editText;
|
||||||
|
if (!isKeyboardShowing(activity)) {
|
||||||
|
showKeyboard(activity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (view instanceof ViewGroup) {
|
||||||
|
ViewGroup group = (ViewGroup) view;
|
||||||
|
for (int i = 0; i < group.getChildCount(); i++) {
|
||||||
|
hookEditTexts(activity, group.getChildAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void hideSystemKeyboard(Activity activity, View view) {
|
||||||
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
||||||
|
Activity.INPUT_METHOD_SERVICE);
|
||||||
|
if (imm != null) {
|
||||||
|
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// Private: Build & Show keyboard
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void showKeyboard(Activity activity) {
|
||||||
|
if (activity == null || activity.isFinishing() || activity.isDestroyed()) return;
|
||||||
|
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
|
||||||
|
// Remove existing keyboard if any
|
||||||
|
View existing = decorView.findViewWithTag(KEYBOARD_OVERLAY_TAG);
|
||||||
|
if (existing != null) {
|
||||||
|
decorView.removeView(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build keyboard
|
||||||
|
LinearLayout keyboard = buildKeyboard(activity);
|
||||||
|
keyboard.setTag(KEYBOARD_OVERLAY_TAG);
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams kbParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
kbParams.gravity = Gravity.BOTTOM;
|
||||||
|
keyboard.setLayoutParams(kbParams);
|
||||||
|
|
||||||
|
// Animate in from bottom
|
||||||
|
decorView.addView(keyboard);
|
||||||
|
keyboard.setTranslationY(400f);
|
||||||
|
keyboard.animate().translationY(0f).setDuration(200).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LinearLayout buildKeyboard(Activity activity) {
|
||||||
|
ThemeManager tm = new ThemeManager(activity);
|
||||||
|
boolean isFoundation = tm.isFoundation();
|
||||||
|
|
||||||
|
int primaryColor = tm.getPrimaryColor(activity);
|
||||||
|
int primaryAlpha15 = Color.argb(38, Color.red(primaryColor),
|
||||||
|
Color.green(primaryColor), Color.blue(primaryColor));
|
||||||
|
int primaryAlpha30 = Color.argb(77, Color.red(primaryColor),
|
||||||
|
Color.green(primaryColor), Color.blue(primaryColor));
|
||||||
|
|
||||||
|
// Glass background colors
|
||||||
|
String glassBg = isFoundation ? "#F2140808" : "#F20D1220";
|
||||||
|
String topBorderColor = isFoundation ? "#4DD4AF37" : "#4D3B82F6";
|
||||||
|
|
||||||
|
Typeface monoFont;
|
||||||
|
try {
|
||||||
|
monoFont = ResourcesCompat.getFont(activity, R.font.source_code_pro);
|
||||||
|
} catch (Exception e) {
|
||||||
|
monoFont = Typeface.MONOSPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container
|
||||||
|
LinearLayout container = new LinearLayout(activity);
|
||||||
|
container.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
container.setTag(KEYBOARD_TAG);
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setColor(Color.parseColor(glassBg));
|
||||||
|
bg.setCornerRadii(new float[]{
|
||||||
|
dpToPx(activity, 12), dpToPx(activity, 12),
|
||||||
|
dpToPx(activity, 12), dpToPx(activity, 12),
|
||||||
|
0, 0, 0, 0
|
||||||
|
});
|
||||||
|
container.setBackground(bg);
|
||||||
|
container.setElevation(dpToPx(activity, 12));
|
||||||
|
container.setPadding(dpToPx(activity, 4), dpToPx(activity, 6),
|
||||||
|
dpToPx(activity, 4), dpToPx(activity, 10));
|
||||||
|
|
||||||
|
// Top border line
|
||||||
|
View topLine = new View(activity);
|
||||||
|
topLine.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(activity, 1)));
|
||||||
|
topLine.setBackgroundColor(Color.parseColor(topBorderColor));
|
||||||
|
container.addView(topLine);
|
||||||
|
|
||||||
|
// ── Key rows ──
|
||||||
|
String[][] rows = showingSymbols ? SYMBOL_ROWS : ALPHA_ROWS;
|
||||||
|
|
||||||
|
for (int rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
||||||
|
LinearLayout row = new LinearLayout(activity);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rowParams.topMargin = dpToPx(activity, 4);
|
||||||
|
row.setLayoutParams(rowParams);
|
||||||
|
|
||||||
|
for (String key : rows[rowIdx]) {
|
||||||
|
View keyView = buildKey(activity, key, monoFont,
|
||||||
|
primaryColor, primaryAlpha15, isFoundation);
|
||||||
|
row.addView(keyView);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addView(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static View buildKey(Activity activity, String key, Typeface font,
|
||||||
|
int primaryColor, int primaryAlpha15, boolean isFoundation) {
|
||||||
|
|
||||||
|
boolean isSpecial = key.equals("⇧") || key.equals("⌫") || key.equals("↵")
|
||||||
|
|| key.equals("?123") || key.equals("ABC");
|
||||||
|
boolean isSpace = key.equals(" ");
|
||||||
|
boolean isEnter = key.equals("↵");
|
||||||
|
boolean isShift = key.equals("⇧");
|
||||||
|
|
||||||
|
// Display text
|
||||||
|
String displayText;
|
||||||
|
if (isSpace) {
|
||||||
|
displayText = "SPACE";
|
||||||
|
} else if (shiftActive && key.length() == 1 && Character.isLetter(key.charAt(0))) {
|
||||||
|
displayText = key.toUpperCase();
|
||||||
|
} else {
|
||||||
|
displayText = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView tv = new TextView(activity);
|
||||||
|
tv.setText(displayText);
|
||||||
|
tv.setGravity(Gravity.CENTER);
|
||||||
|
tv.setTypeface(font);
|
||||||
|
|
||||||
|
// Sizing
|
||||||
|
int height = dpToPx(activity, 42);
|
||||||
|
float weight;
|
||||||
|
|
||||||
|
if (isSpace) {
|
||||||
|
weight = 5f;
|
||||||
|
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
tv.setLetterSpacing(0.15f);
|
||||||
|
} else if (key.equals("?123") || key.equals("ABC")) {
|
||||||
|
weight = 1.5f;
|
||||||
|
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
} else if (isShift || key.equals("⌫")) {
|
||||||
|
weight = 1.3f;
|
||||||
|
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||||
|
} else if (isEnter) {
|
||||||
|
weight = 1.3f;
|
||||||
|
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||||
|
} else {
|
||||||
|
weight = 1f;
|
||||||
|
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
0, height, weight);
|
||||||
|
params.setMargins(dpToPx(activity, 2), dpToPx(activity, 2),
|
||||||
|
dpToPx(activity, 2), dpToPx(activity, 2));
|
||||||
|
tv.setLayoutParams(params);
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
GradientDrawable keyBg = new GradientDrawable();
|
||||||
|
keyBg.setCornerRadius(dpToPx(activity, 6));
|
||||||
|
|
||||||
|
if (isEnter) {
|
||||||
|
keyBg.setColor(primaryColor);
|
||||||
|
tv.setTextColor(Color.WHITE);
|
||||||
|
} else if (isSpecial || isSpace) {
|
||||||
|
keyBg.setColor(primaryAlpha15);
|
||||||
|
tv.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
} else {
|
||||||
|
keyBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
tv.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift active indicator
|
||||||
|
if (isShift && shiftActive) {
|
||||||
|
keyBg.setStroke(dpToPx(activity, 1), primaryColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
tv.setBackground(keyBg);
|
||||||
|
|
||||||
|
// Touch feedback + key action
|
||||||
|
tv.setOnTouchListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
|
keyBg.setColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
tv.setBackground(keyBg);
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_UP
|
||||||
|
|| event.getAction() == MotionEvent.ACTION_CANCEL) {
|
||||||
|
// Restore
|
||||||
|
if (isEnter) {
|
||||||
|
keyBg.setColor(primaryColor);
|
||||||
|
} else if (isSpecial || isSpace) {
|
||||||
|
keyBg.setColor(primaryAlpha15);
|
||||||
|
} else {
|
||||||
|
keyBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
}
|
||||||
|
tv.setBackground(keyBg);
|
||||||
|
}
|
||||||
|
return false; // Let onClick also fire
|
||||||
|
});
|
||||||
|
|
||||||
|
tv.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
handleKeyPress(activity, key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return tv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// Private: Key press handling
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void handleKeyPress(Activity activity, String key) {
|
||||||
|
if (currentEditText == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "⇧":
|
||||||
|
shiftActive = !shiftActive;
|
||||||
|
rebuildKeyboard(activity);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "⌫":
|
||||||
|
handleBackspace();
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "↵":
|
||||||
|
handleEnter(activity);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "?123":
|
||||||
|
showingSymbols = true;
|
||||||
|
rebuildKeyboard(activity);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "ABC":
|
||||||
|
showingSymbols = false;
|
||||||
|
rebuildKeyboard(activity);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case " ":
|
||||||
|
insertText(" ");
|
||||||
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
String text = key;
|
||||||
|
if (shiftActive && key.length() == 1 && Character.isLetter(key.charAt(0))) {
|
||||||
|
text = key.toUpperCase();
|
||||||
|
shiftActive = false;
|
||||||
|
rebuildKeyboard(activity);
|
||||||
|
}
|
||||||
|
insertText(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void insertText(String text) {
|
||||||
|
if (currentEditText == null) return;
|
||||||
|
Editable editable = currentEditText.getText();
|
||||||
|
int start = currentEditText.getSelectionStart();
|
||||||
|
int end = currentEditText.getSelectionEnd();
|
||||||
|
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
if (end < 0) end = start;
|
||||||
|
|
||||||
|
if (start != end) {
|
||||||
|
// Replace selection
|
||||||
|
editable.replace(start, end, text);
|
||||||
|
} else {
|
||||||
|
editable.insert(start, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleBackspace() {
|
||||||
|
if (currentEditText == null) return;
|
||||||
|
Editable editable = currentEditText.getText();
|
||||||
|
int start = currentEditText.getSelectionStart();
|
||||||
|
int end = currentEditText.getSelectionEnd();
|
||||||
|
|
||||||
|
if (start != end && start >= 0 && end >= 0) {
|
||||||
|
// Delete selection
|
||||||
|
editable.delete(Math.min(start, end), Math.max(start, end));
|
||||||
|
} else if (start > 0) {
|
||||||
|
editable.delete(start - 1, start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleEnter(Activity activity) {
|
||||||
|
if (currentEditText == null) return;
|
||||||
|
|
||||||
|
// Check if the EditText has a single-line IME action
|
||||||
|
int imeOptions = currentEditText.getImeOptions();
|
||||||
|
int inputType = currentEditText.getInputType();
|
||||||
|
|
||||||
|
// For single-line fields, fire the IME action (like Send, Go, etc.)
|
||||||
|
boolean isSingleLine = (inputType & android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0;
|
||||||
|
|
||||||
|
if (isSingleLine) {
|
||||||
|
// Dispatch the editor action
|
||||||
|
currentEditText.onEditorAction(imeOptions & EditorInfo.IME_MASK_ACTION);
|
||||||
|
} else {
|
||||||
|
// Multi-line: insert newline
|
||||||
|
insertText("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void rebuildKeyboard(Activity activity) {
|
||||||
|
if (activity == null || activity.isFinishing() || activity.isDestroyed()) return;
|
||||||
|
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
View existing = decorView.findViewWithTag(KEYBOARD_OVERLAY_TAG);
|
||||||
|
if (existing != null) {
|
||||||
|
decorView.removeView(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout keyboard = buildKeyboard(activity);
|
||||||
|
keyboard.setTag(KEYBOARD_OVERLAY_TAG);
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams kbParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
kbParams.gravity = Gravity.BOTTOM;
|
||||||
|
keyboard.setLayoutParams(kbParams);
|
||||||
|
|
||||||
|
decorView.addView(keyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// Utility
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static int dpToPx(Activity activity, float dp) {
|
||||||
|
return Math.round(TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, dp,
|
||||||
|
activity.getResources().getDisplayMetrics()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.service.notification.NotificationListenerService;
|
||||||
|
import android.service.notification.StatusBarNotification;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts real Android notifications so AeThexOS can display them
|
||||||
|
* in its own notification panel instead of the Android shade.
|
||||||
|
*/
|
||||||
|
public class AeThexNotificationService extends NotificationListenerService {
|
||||||
|
|
||||||
|
private static AeThexNotificationService instance;
|
||||||
|
private static final List<NotificationData> activeNotifications = new ArrayList<>();
|
||||||
|
private static OnNotificationChangeListener listener;
|
||||||
|
|
||||||
|
public static class NotificationData {
|
||||||
|
public String packageName;
|
||||||
|
public String title;
|
||||||
|
public String text;
|
||||||
|
public long postTime;
|
||||||
|
public String key;
|
||||||
|
|
||||||
|
public NotificationData(String packageName, String title, String text, long postTime, String key) {
|
||||||
|
this.packageName = packageName;
|
||||||
|
this.title = title;
|
||||||
|
this.text = text;
|
||||||
|
this.postTime = postTime;
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnNotificationChangeListener {
|
||||||
|
void onNotificationsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setListener(OnNotificationChangeListener l) {
|
||||||
|
listener = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<NotificationData> getNotifications() {
|
||||||
|
synchronized (activeNotifications) {
|
||||||
|
return new ArrayList<>(activeNotifications);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getCount() {
|
||||||
|
synchronized (activeNotifications) {
|
||||||
|
return activeNotifications.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isRunning() {
|
||||||
|
return instance != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNotificationPosted(StatusBarNotification sbn) {
|
||||||
|
Notification notification = sbn.getNotification();
|
||||||
|
if (notification == null) return;
|
||||||
|
|
||||||
|
Bundle extras = notification.extras;
|
||||||
|
String title = extras != null ? extras.getString(Notification.EXTRA_TITLE, "") : "";
|
||||||
|
CharSequence textCs = extras != null ? extras.getCharSequence(Notification.EXTRA_TEXT) : null;
|
||||||
|
String text = textCs != null ? textCs.toString() : "";
|
||||||
|
|
||||||
|
// Skip empty notifications
|
||||||
|
if (title.isEmpty() && text.isEmpty()) return;
|
||||||
|
|
||||||
|
// Skip our own notifications
|
||||||
|
if (sbn.getPackageName().equals(getPackageName())) return;
|
||||||
|
|
||||||
|
synchronized (activeNotifications) {
|
||||||
|
// Remove existing with same key
|
||||||
|
activeNotifications.removeIf(n -> n.key.equals(sbn.getKey()));
|
||||||
|
// Add new
|
||||||
|
activeNotifications.add(0, new NotificationData(
|
||||||
|
sbn.getPackageName(), title, text, sbn.getPostTime(), sbn.getKey()));
|
||||||
|
// Cap at 20
|
||||||
|
while (activeNotifications.size() > 20) {
|
||||||
|
activeNotifications.remove(activeNotifications.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener != null) listener.onNotificationsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNotificationRemoved(StatusBarNotification sbn) {
|
||||||
|
synchronized (activeNotifications) {
|
||||||
|
activeNotifications.removeIf(n -> n.key.equals(sbn.getKey()));
|
||||||
|
}
|
||||||
|
if (listener != null) listener.onNotificationsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListenerConnected() {
|
||||||
|
// Load existing notifications
|
||||||
|
try {
|
||||||
|
StatusBarNotification[] current = super.getActiveNotifications();
|
||||||
|
if (current != null) {
|
||||||
|
synchronized (activeNotifications) {
|
||||||
|
activeNotifications.clear();
|
||||||
|
for (StatusBarNotification sbn : current) {
|
||||||
|
Notification n = sbn.getNotification();
|
||||||
|
if (n == null) continue;
|
||||||
|
Bundle extras = n.extras;
|
||||||
|
String title = extras != null ? extras.getString(Notification.EXTRA_TITLE, "") : "";
|
||||||
|
CharSequence textCs = extras != null ? extras.getCharSequence(Notification.EXTRA_TEXT) : null;
|
||||||
|
String text = textCs != null ? textCs.toString() : "";
|
||||||
|
if (title.isEmpty() && text.isEmpty()) continue;
|
||||||
|
if (sbn.getPackageName().equals(getPackageName())) continue;
|
||||||
|
activeNotifications.add(new NotificationData(
|
||||||
|
sbn.getPackageName(), title, text, sbn.getPostTime(), sbn.getKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
if (listener != null) listener.onNotificationsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
208
android/app/src/main/java/com/aethex/os/AeThexToast.java
Normal file
208
android/app/src/main/java/com/aethex/os/AeThexToast.java
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
|
|
||||||
|
public class AeThexToast {
|
||||||
|
|
||||||
|
private static final String CONTAINER_TAG = "aethex_toast_container";
|
||||||
|
private static final int ANIM_DURATION = 200;
|
||||||
|
private static final int DISPLAY_DURATION = 4000;
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
INFO("#06B6D4"),
|
||||||
|
SUCCESS("#22C55E"),
|
||||||
|
WARNING("#FBBF24"),
|
||||||
|
ERROR("#EF4444");
|
||||||
|
|
||||||
|
public final String color;
|
||||||
|
|
||||||
|
Type(String color) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void show(Activity activity, String message, Type type) {
|
||||||
|
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
LinearLayout container = getOrCreateContainer(activity);
|
||||||
|
View toastView = createToastView(activity, message, type);
|
||||||
|
container.addView(toastView);
|
||||||
|
|
||||||
|
// Animate in: slide from right + fade in
|
||||||
|
toastView.setTranslationX(100f);
|
||||||
|
toastView.setAlpha(0f);
|
||||||
|
toastView.animate()
|
||||||
|
.translationX(0f)
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(ANIM_DURATION)
|
||||||
|
.start();
|
||||||
|
|
||||||
|
// Auto-dismiss after delay
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
toastView.animate()
|
||||||
|
.translationX(100f)
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(ANIM_DURATION)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
ViewGroup parent = (ViewGroup) toastView.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.removeView(toastView);
|
||||||
|
if (parent.getChildCount() == 0) {
|
||||||
|
ViewGroup grandParent = (ViewGroup) parent.getParent();
|
||||||
|
if (grandParent != null) {
|
||||||
|
grandParent.removeView(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}, DISPLAY_DURATION);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LinearLayout getOrCreateContainer(Activity activity) {
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
View existing = decorView.findViewWithTag(CONTAINER_TAG);
|
||||||
|
|
||||||
|
if (existing instanceof LinearLayout) {
|
||||||
|
return (LinearLayout) existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout container = new LinearLayout(activity);
|
||||||
|
container.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
container.setGravity(Gravity.TOP | Gravity.END);
|
||||||
|
container.setTag(CONTAINER_TAG);
|
||||||
|
|
||||||
|
int paddingTop = dpToPx(activity, 16);
|
||||||
|
int paddingEnd = dpToPx(activity, 12);
|
||||||
|
container.setPaddingRelative(0, paddingTop, paddingEnd, 0);
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
params.gravity = Gravity.TOP | Gravity.END;
|
||||||
|
container.setLayoutParams(params);
|
||||||
|
|
||||||
|
container.setClickable(false);
|
||||||
|
container.setFocusable(false);
|
||||||
|
|
||||||
|
decorView.addView(container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static View createToastView(Activity activity, String message, Type type) {
|
||||||
|
int dp1 = dpToPx(activity, 1);
|
||||||
|
int dp4 = dpToPx(activity, 4);
|
||||||
|
int dp8 = dpToPx(activity, 8);
|
||||||
|
int dp12 = dpToPx(activity, 12);
|
||||||
|
int maxWidth = dpToPx(activity, 280);
|
||||||
|
|
||||||
|
// ── Resolve theme colors ──
|
||||||
|
ThemeManager tm = new ThemeManager(activity);
|
||||||
|
boolean isFoundation = tm.isFoundation();
|
||||||
|
|
||||||
|
// Glass background + border adapt to clearance mode
|
||||||
|
String glassColor = isFoundation ? "#E61A0A0A" : "#E60F172A";
|
||||||
|
String borderColor = isFoundation ? "#33D4AF37" : "#333B82F6";
|
||||||
|
int themeGlowColor = isFoundation
|
||||||
|
? Color.parseColor("#33D4AF37") // gold glow
|
||||||
|
: Color.parseColor("#333B82F6"); // blue glow
|
||||||
|
|
||||||
|
// Outer wrapper with themed glass background
|
||||||
|
LinearLayout outerWrapper = new LinearLayout(activity);
|
||||||
|
outerWrapper.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
GradientDrawable borderDrawable = new GradientDrawable();
|
||||||
|
borderDrawable.setShape(GradientDrawable.RECTANGLE);
|
||||||
|
borderDrawable.setCornerRadius(dpToPx(activity, 8));
|
||||||
|
borderDrawable.setStroke(dp1, Color.parseColor(borderColor));
|
||||||
|
borderDrawable.setColor(Color.parseColor(glassColor));
|
||||||
|
outerWrapper.setBackground(borderDrawable);
|
||||||
|
outerWrapper.setElevation(dp4);
|
||||||
|
outerWrapper.setClipToOutline(true);
|
||||||
|
|
||||||
|
// Toast horizontal layout (accent bar + message)
|
||||||
|
LinearLayout toastLayout = new LinearLayout(activity);
|
||||||
|
toastLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
toastLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
toastLayout.setPadding(0, dp12, dp12, dp12);
|
||||||
|
|
||||||
|
// Accent bar: 4dp wide, full height, colored by type
|
||||||
|
View accentBar = new View(activity);
|
||||||
|
LinearLayout.LayoutParams accentParams = new LinearLayout.LayoutParams(
|
||||||
|
dp4, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||||
|
accentBar.setLayoutParams(accentParams);
|
||||||
|
accentBar.setBackgroundColor(Color.parseColor(type.color));
|
||||||
|
|
||||||
|
// Message TextView
|
||||||
|
TextView messageView = new TextView(activity);
|
||||||
|
messageView.setText(message);
|
||||||
|
messageView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
messageView.setTextColor(Color.argb((int) (255 * 0.8f), 255, 255, 255));
|
||||||
|
messageView.setMaxWidth(maxWidth - dp4 - dp12 - dp12);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Typeface font = ResourcesCompat.getFont(activity, R.font.source_code_pro);
|
||||||
|
if (font != null) {
|
||||||
|
messageView.setTypeface(font);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
messageView.setTypeface(Typeface.MONOSPACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
textParams.setMarginStart(dp12);
|
||||||
|
messageView.setLayoutParams(textParams);
|
||||||
|
|
||||||
|
toastLayout.addView(accentBar);
|
||||||
|
toastLayout.addView(messageView);
|
||||||
|
|
||||||
|
outerWrapper.addView(toastLayout, new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
// ── Bottom glow line (clearance-colored, 1dp) ──
|
||||||
|
View glowLine = new View(activity);
|
||||||
|
LinearLayout.LayoutParams glowParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, dp1);
|
||||||
|
glowLine.setLayoutParams(glowParams);
|
||||||
|
glowLine.setBackgroundColor(themeGlowColor);
|
||||||
|
outerWrapper.addView(glowLine);
|
||||||
|
|
||||||
|
// Container item params with 8dp bottom spacing between toasts
|
||||||
|
LinearLayout.LayoutParams containerItemParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
containerItemParams.setMargins(0, 0, 0, dp8);
|
||||||
|
containerItemParams.gravity = Gravity.END;
|
||||||
|
outerWrapper.setLayoutParams(containerItemParams);
|
||||||
|
|
||||||
|
return outerWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int dpToPx(Activity activity, float dp) {
|
||||||
|
return Math.round(TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, dp,
|
||||||
|
activity.getResources().getDisplayMetrics()));
|
||||||
|
}
|
||||||
|
}
|
||||||
671
android/app/src/main/java/com/aethex/os/AnalyticsActivity.java
Normal file
671
android/app/src/main/java/com/aethex/os/AnalyticsActivity.java
Normal file
|
|
@ -0,0 +1,671 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.view.animation.DecelerateInterpolator;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analytics -- Live simulated analytics dashboard with stats, bar chart,
|
||||||
|
* network metrics, live activity log, and export functionality.
|
||||||
|
*/
|
||||||
|
public class AnalyticsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private Handler handler;
|
||||||
|
private Random random = new Random();
|
||||||
|
|
||||||
|
// Stat card value TextViews
|
||||||
|
private TextView statUsers;
|
||||||
|
private TextView statSessions;
|
||||||
|
private TextView statUptime;
|
||||||
|
private TextView statRequests;
|
||||||
|
|
||||||
|
// Network metric TextViews
|
||||||
|
private TextView netBandwidth;
|
||||||
|
private TextView netLatency;
|
||||||
|
private TextView netPacketLoss;
|
||||||
|
|
||||||
|
// Live activity log
|
||||||
|
private LinearLayout logContainer;
|
||||||
|
private final List<View> logEntries = new ArrayList<>();
|
||||||
|
private static final int MAX_LOG_ENTRIES = 10;
|
||||||
|
|
||||||
|
// Bar chart bars for animation
|
||||||
|
private final List<View> chartBars = new ArrayList<>();
|
||||||
|
private final List<Integer> chartTargetHeights = new ArrayList<>();
|
||||||
|
|
||||||
|
// Stat breakdown details for tap-to-inspect
|
||||||
|
private static final String[] STAT_LABELS = {"ACTIVE USERS", "SESSIONS", "UPTIME", "REQUESTS/S"};
|
||||||
|
|
||||||
|
// Log message templates
|
||||||
|
private static final String[] LOG_USERS = {
|
||||||
|
"architect@aethex", "admin@corp", "dev.team@aethex", "ops@foundation",
|
||||||
|
"security@aethex", "analyst@corp", "root@system", "ci-bot@pipeline",
|
||||||
|
"monitor@infra", "deploy@staging"
|
||||||
|
};
|
||||||
|
private static final String[] LOG_ACTIONS = {
|
||||||
|
"User login", "API call", "Module loaded", "Session start",
|
||||||
|
"Config update", "Cache cleared", "DB query", "Auth token refresh",
|
||||||
|
"Webhook fired", "Service restart", "Log rotation", "Health check",
|
||||||
|
"Schema migration", "Backup snapshot", "Rate limit hit"
|
||||||
|
};
|
||||||
|
private static final String[] LOG_TARGETS = {
|
||||||
|
"/v2/analytics/stream", "cipher_toolkit", "device:android",
|
||||||
|
"theme_engine", "/api/v1/users", "auth_service", "redis_cache",
|
||||||
|
"/v2/telemetry", "kernel_module", "mesh_network", "/v1/health",
|
||||||
|
"storage_layer", "dns_resolver", "proxy_gateway", "cert_manager"
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
handler = new Handler(Looper.getMainLooper());
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
TextView title = findViewById(R.id.app_title);
|
||||||
|
title.setText("Analytics");
|
||||||
|
TextView nameDisplay = findViewById(R.id.app_name_display);
|
||||||
|
|
||||||
|
findViewById(R.id.app_back).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout content = (LinearLayout) nameDisplay.getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
content.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
|
||||||
|
content.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Wrap everything in a ScrollView
|
||||||
|
ScrollView scrollView = new ScrollView(this);
|
||||||
|
scrollView.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
scrollView.setFillViewport(true);
|
||||||
|
scrollView.setVerticalScrollBarEnabled(false);
|
||||||
|
|
||||||
|
LinearLayout scrollContent = new LinearLayout(this);
|
||||||
|
scrollContent.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
scrollContent.setLayoutParams(new ScrollView.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
scrollContent.setPadding(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(24));
|
||||||
|
|
||||||
|
buildAnalyticsUI(scrollContent);
|
||||||
|
|
||||||
|
scrollView.addView(scrollContent);
|
||||||
|
content.addView(scrollView);
|
||||||
|
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
// Start live updaters
|
||||||
|
startStatsUpdater();
|
||||||
|
startNetworkUpdater();
|
||||||
|
startLogUpdater();
|
||||||
|
|
||||||
|
// Animate bar chart on entry (delayed to let layout settle)
|
||||||
|
handler.postDelayed(this::animateChartBars, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// BUILD UI
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void buildAnalyticsUI(LinearLayout parent) {
|
||||||
|
Typeface displayFont = themeManager.getDisplayFont(this);
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(this);
|
||||||
|
|
||||||
|
// ── Section: LIVE DASHBOARD header ──
|
||||||
|
addSectionHeader(parent, "LIVE DASHBOARD", monoFont);
|
||||||
|
|
||||||
|
// ── Stats grid -- 2x2 ──
|
||||||
|
LinearLayout row1 = createHorizontalRow(dpToPx(10));
|
||||||
|
statUsers = createStatCard(row1, STAT_LABELS[0], "1,247", "#06B6D4", displayFont, monoFont, 0);
|
||||||
|
statSessions = createStatCard(row1, STAT_LABELS[1], "3,891", "#22C55E", displayFont, monoFont, 1);
|
||||||
|
parent.addView(row1);
|
||||||
|
|
||||||
|
LinearLayout row2 = createHorizontalRow(dpToPx(16));
|
||||||
|
statUptime = createStatCard(row2, STAT_LABELS[2], "99.97%", "#A855F7", displayFont, monoFont, 2);
|
||||||
|
statRequests = createStatCard(row2, STAT_LABELS[3], "842", "#F97316", displayFont, monoFont, 3);
|
||||||
|
parent.addView(row2);
|
||||||
|
|
||||||
|
// ── Section: TRAFFIC (bar chart) ──
|
||||||
|
addDivider(parent);
|
||||||
|
addSectionHeader(parent, "WEEKLY TRAFFIC", monoFont);
|
||||||
|
parent.addView(buildBarChart(displayFont, monoFont));
|
||||||
|
|
||||||
|
// ── Section: NETWORK ──
|
||||||
|
addDivider(parent);
|
||||||
|
addSectionHeader(parent, "NETWORK", monoFont);
|
||||||
|
parent.addView(buildNetworkSection(displayFont, monoFont));
|
||||||
|
|
||||||
|
// ── Section: RECENT ACTIVITY (live log) ──
|
||||||
|
addDivider(parent);
|
||||||
|
addSectionHeader(parent, "RECENT ACTIVITY", monoFont);
|
||||||
|
logContainer = new LinearLayout(this);
|
||||||
|
logContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
logContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
parent.addView(logContainer);
|
||||||
|
|
||||||
|
// Seed initial log entries
|
||||||
|
seedInitialLogEntries(monoFont);
|
||||||
|
|
||||||
|
// ── Export Report button ──
|
||||||
|
LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(48));
|
||||||
|
btnParams.topMargin = dpToPx(24);
|
||||||
|
parent.addView(buildExportButton(monoFont, btnParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// STAT CARDS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private TextView createStatCard(LinearLayout parent, String label, String value,
|
||||||
|
String colorHex, Typeface displayFont, Typeface monoFont,
|
||||||
|
int statIndex) {
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
card.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
cardParams.setMarginEnd(dpToPx(8));
|
||||||
|
card.setLayoutParams(cardParams);
|
||||||
|
card.setPadding(dpToPx(12), dpToPx(16), dpToPx(12), dpToPx(16));
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(12));
|
||||||
|
bg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
card.setBackground(bg);
|
||||||
|
|
||||||
|
TextView valueView = new TextView(this);
|
||||||
|
valueView.setText(value);
|
||||||
|
valueView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 22);
|
||||||
|
valueView.setTextColor(Color.parseColor(colorHex));
|
||||||
|
valueView.setTypeface(displayFont);
|
||||||
|
valueView.setGravity(Gravity.CENTER);
|
||||||
|
valueView.setTag("stat_value");
|
||||||
|
card.addView(valueView);
|
||||||
|
|
||||||
|
TextView labelView = new TextView(this);
|
||||||
|
labelView.setText(label);
|
||||||
|
labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
labelView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
labelView.setTypeface(monoFont);
|
||||||
|
labelView.setGravity(Gravity.CENTER);
|
||||||
|
labelView.setLetterSpacing(0.1f);
|
||||||
|
LinearLayout.LayoutParams lParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
lParams.topMargin = dpToPx(4);
|
||||||
|
labelView.setLayoutParams(lParams);
|
||||||
|
card.addView(labelView);
|
||||||
|
|
||||||
|
// Tappable: show detailed breakdown
|
||||||
|
card.setClickable(true);
|
||||||
|
card.setFocusable(true);
|
||||||
|
card.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
String detail = getStatBreakdown(statIndex, valueView.getText().toString());
|
||||||
|
AeThexToast.show(AnalyticsActivity.this, detail, AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.addView(card);
|
||||||
|
return valueView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getStatBreakdown(int statIndex, String currentValue) {
|
||||||
|
switch (statIndex) {
|
||||||
|
case 0: // Active Users
|
||||||
|
int peakHour = 10 + random.nextInt(10);
|
||||||
|
int peakUsers = 1400 + random.nextInt(200);
|
||||||
|
return "Active Users: " + currentValue
|
||||||
|
+ " \u2014 Peak: " + String.format(Locale.US, "%,d", peakUsers)
|
||||||
|
+ " at " + String.format(Locale.US, "%02d:00", peakHour);
|
||||||
|
case 1: // Sessions
|
||||||
|
int avgDuration = 4 + random.nextInt(8);
|
||||||
|
int bounceRate = 15 + random.nextInt(20);
|
||||||
|
return "Sessions: " + currentValue
|
||||||
|
+ " \u2014 Avg duration: " + avgDuration + "m"
|
||||||
|
+ " \u2014 Bounce: " + bounceRate + "%";
|
||||||
|
case 2: // Uptime
|
||||||
|
int daysUp = 30 + random.nextInt(60);
|
||||||
|
return "Uptime: " + currentValue
|
||||||
|
+ " \u2014 " + daysUp + " days since last restart"
|
||||||
|
+ " \u2014 SLA target: 99.95%";
|
||||||
|
case 3: // Requests/s
|
||||||
|
int p99 = 80 + random.nextInt(120);
|
||||||
|
int errRate = random.nextInt(3);
|
||||||
|
return "Requests/s: " + currentValue
|
||||||
|
+ " \u2014 p99 latency: " + p99 + "ms"
|
||||||
|
+ " \u2014 Error rate: 0." + errRate + "%";
|
||||||
|
default:
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// BAR CHART
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private LinearLayout buildBarChart(Typeface displayFont, Typeface monoFont) {
|
||||||
|
String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
|
||||||
|
String[] barColors = {"#06B6D4", "#22C55E", "#A855F7", "#F97316", "#EF4444", "#FBBF24", "#06B6D4"};
|
||||||
|
int maxBarHeight = dpToPx(100);
|
||||||
|
int minBarHeight = dpToPx(20);
|
||||||
|
|
||||||
|
LinearLayout chartContainer = new LinearLayout(this);
|
||||||
|
chartContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
chartContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
// Card background
|
||||||
|
GradientDrawable chartBg = new GradientDrawable();
|
||||||
|
chartBg.setCornerRadius(dpToPx(12));
|
||||||
|
chartBg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
chartBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
chartContainer.setBackground(chartBg);
|
||||||
|
chartContainer.setPadding(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(12));
|
||||||
|
|
||||||
|
// Row of bars
|
||||||
|
LinearLayout barsRow = new LinearLayout(this);
|
||||||
|
barsRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
barsRow.setGravity(Gravity.BOTTOM);
|
||||||
|
barsRow.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, maxBarHeight + dpToPx(4)));
|
||||||
|
|
||||||
|
for (int i = 0; i < days.length; i++) {
|
||||||
|
int targetHeight = minBarHeight + random.nextInt(maxBarHeight - minBarHeight);
|
||||||
|
|
||||||
|
LinearLayout barColumn = new LinearLayout(this);
|
||||||
|
barColumn.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
barColumn.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
|
||||||
|
LinearLayout.LayoutParams colParams = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f);
|
||||||
|
colParams.setMarginStart(dpToPx(3));
|
||||||
|
colParams.setMarginEnd(dpToPx(3));
|
||||||
|
barColumn.setLayoutParams(colParams);
|
||||||
|
|
||||||
|
// The bar itself
|
||||||
|
View bar = new View(this);
|
||||||
|
GradientDrawable barDrawable = new GradientDrawable();
|
||||||
|
barDrawable.setCornerRadii(new float[]{
|
||||||
|
dpToPx(4), dpToPx(4), dpToPx(4), dpToPx(4), 0, 0, 0, 0});
|
||||||
|
barDrawable.setColor(Color.parseColor(barColors[i]));
|
||||||
|
bar.setBackground(barDrawable);
|
||||||
|
|
||||||
|
// Start at 0 height for animation
|
||||||
|
LinearLayout.LayoutParams barParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0);
|
||||||
|
bar.setLayoutParams(barParams);
|
||||||
|
barColumn.addView(bar);
|
||||||
|
|
||||||
|
chartBars.add(bar);
|
||||||
|
chartTargetHeights.add(targetHeight);
|
||||||
|
|
||||||
|
barsRow.addView(barColumn);
|
||||||
|
}
|
||||||
|
chartContainer.addView(barsRow);
|
||||||
|
|
||||||
|
// Day labels row
|
||||||
|
LinearLayout labelsRow = new LinearLayout(this);
|
||||||
|
labelsRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
LinearLayout.LayoutParams labelsParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
labelsParams.topMargin = dpToPx(8);
|
||||||
|
labelsRow.setLayoutParams(labelsParams);
|
||||||
|
|
||||||
|
for (String day : days) {
|
||||||
|
TextView dayLabel = new TextView(this);
|
||||||
|
dayLabel.setText(day);
|
||||||
|
dayLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
dayLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
dayLabel.setTypeface(monoFont);
|
||||||
|
dayLabel.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams dlParams = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
dayLabel.setLayoutParams(dlParams);
|
||||||
|
labelsRow.addView(dayLabel);
|
||||||
|
}
|
||||||
|
chartContainer.addView(labelsRow);
|
||||||
|
|
||||||
|
return chartContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateChartBars() {
|
||||||
|
for (int i = 0; i < chartBars.size(); i++) {
|
||||||
|
final View bar = chartBars.get(i);
|
||||||
|
final int targetHeight = chartTargetHeights.get(i);
|
||||||
|
|
||||||
|
ValueAnimator animator = ValueAnimator.ofInt(0, targetHeight);
|
||||||
|
animator.setDuration(400);
|
||||||
|
animator.setStartDelay(i * 50L);
|
||||||
|
animator.setInterpolator(new DecelerateInterpolator());
|
||||||
|
animator.addUpdateListener(animation -> {
|
||||||
|
ViewGroup.LayoutParams params = bar.getLayoutParams();
|
||||||
|
params.height = (int) animation.getAnimatedValue();
|
||||||
|
bar.setLayoutParams(params);
|
||||||
|
});
|
||||||
|
animator.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// NETWORK SECTION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private LinearLayout buildNetworkSection(Typeface displayFont, Typeface monoFont) {
|
||||||
|
LinearLayout networkRow = createHorizontalRow(dpToPx(16));
|
||||||
|
|
||||||
|
netBandwidth = createNetworkMetricCard(networkRow, "BANDWIDTH", "12.4 MB/s", "#06B6D4", displayFont, monoFont);
|
||||||
|
netLatency = createNetworkMetricCard(networkRow, "LATENCY", "23ms", "#22C55E", displayFont, monoFont);
|
||||||
|
netPacketLoss = createNetworkMetricCard(networkRow, "PACKET LOSS", "0.01%", "#FBBF24", displayFont, monoFont);
|
||||||
|
|
||||||
|
return networkRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextView createNetworkMetricCard(LinearLayout parent, String label, String value,
|
||||||
|
String colorHex, Typeface displayFont, Typeface monoFont) {
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
card.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
cardParams.setMarginEnd(dpToPx(6));
|
||||||
|
card.setLayoutParams(cardParams);
|
||||||
|
card.setPadding(dpToPx(8), dpToPx(12), dpToPx(8), dpToPx(12));
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(12));
|
||||||
|
bg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
card.setBackground(bg);
|
||||||
|
|
||||||
|
TextView valueView = new TextView(this);
|
||||||
|
valueView.setText(value);
|
||||||
|
valueView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15);
|
||||||
|
valueView.setTextColor(Color.parseColor(colorHex));
|
||||||
|
valueView.setTypeface(displayFont);
|
||||||
|
valueView.setGravity(Gravity.CENTER);
|
||||||
|
card.addView(valueView);
|
||||||
|
|
||||||
|
TextView labelView = new TextView(this);
|
||||||
|
labelView.setText(label);
|
||||||
|
labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 8);
|
||||||
|
labelView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
labelView.setTypeface(monoFont);
|
||||||
|
labelView.setGravity(Gravity.CENTER);
|
||||||
|
labelView.setLetterSpacing(0.1f);
|
||||||
|
LinearLayout.LayoutParams lParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
lParams.topMargin = dpToPx(4);
|
||||||
|
labelView.setLayoutParams(lParams);
|
||||||
|
card.addView(labelView);
|
||||||
|
|
||||||
|
parent.addView(card);
|
||||||
|
return valueView;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// LIVE ACTIVITY LOG
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void seedInitialLogEntries(Typeface monoFont) {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
long ts = now - (i * 15000L) - random.nextInt(10000);
|
||||||
|
String time = sdf.format(new Date(ts));
|
||||||
|
String entry = time + " " + LOG_ACTIONS[random.nextInt(LOG_ACTIONS.length)]
|
||||||
|
+ " \u2014 " + LOG_TARGETS[random.nextInt(LOG_TARGETS.length)];
|
||||||
|
addLogEntryView(entry, monoFont, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLogEntryView(String text, Typeface monoFont, boolean animate) {
|
||||||
|
TextView logLine = new TextView(this);
|
||||||
|
logLine.setText(text);
|
||||||
|
logLine.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
logLine.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
logLine.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
llParams.bottomMargin = dpToPx(4);
|
||||||
|
logLine.setLayoutParams(llParams);
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
logLine.setAlpha(0f);
|
||||||
|
logLine.setTranslationY(-dpToPx(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepend (add at index 0)
|
||||||
|
logContainer.addView(logLine, 0);
|
||||||
|
logEntries.add(0, logLine);
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
logLine.animate().alpha(1f).translationY(0).setDuration(250).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim to max entries
|
||||||
|
while (logEntries.size() > MAX_LOG_ENTRIES) {
|
||||||
|
View old = logEntries.remove(logEntries.size() - 1);
|
||||||
|
logContainer.removeView(old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateLogEntry() {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||||
|
String time = sdf.format(new Date());
|
||||||
|
String action = LOG_ACTIONS[random.nextInt(LOG_ACTIONS.length)];
|
||||||
|
String target = LOG_TARGETS[random.nextInt(LOG_TARGETS.length)];
|
||||||
|
|
||||||
|
// Occasionally include a user
|
||||||
|
if (random.nextInt(3) == 0) {
|
||||||
|
String user = LOG_USERS[random.nextInt(LOG_USERS.length)];
|
||||||
|
return time + " " + action + " \u2014 " + user;
|
||||||
|
}
|
||||||
|
return time + " " + action + " \u2014 " + target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// EXPORT BUTTON
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private LinearLayout buildExportButton(Typeface monoFont, LinearLayout.LayoutParams params) {
|
||||||
|
LinearLayout button = new LinearLayout(this);
|
||||||
|
button.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
button.setGravity(Gravity.CENTER);
|
||||||
|
button.setLayoutParams(params);
|
||||||
|
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
|
||||||
|
GradientDrawable btnBg = new GradientDrawable();
|
||||||
|
btnBg.setCornerRadius(dpToPx(10));
|
||||||
|
btnBg.setColor(Color.argb(40,
|
||||||
|
Color.red(primaryColor), Color.green(primaryColor), Color.blue(primaryColor)));
|
||||||
|
btnBg.setStroke(dpToPx(1), Color.argb(80,
|
||||||
|
Color.red(primaryColor), Color.green(primaryColor), Color.blue(primaryColor)));
|
||||||
|
button.setBackground(btnBg);
|
||||||
|
|
||||||
|
TextView btnText = new TextView(this);
|
||||||
|
btnText.setText("EXPORT REPORT");
|
||||||
|
btnText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
btnText.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
btnText.setTypeface(monoFont);
|
||||||
|
btnText.setLetterSpacing(0.15f);
|
||||||
|
button.addView(btnText);
|
||||||
|
|
||||||
|
button.setClickable(true);
|
||||||
|
button.setFocusable(true);
|
||||||
|
button.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
AeThexToast.show(AnalyticsActivity.this,
|
||||||
|
"Report exported to /system/reports/", AeThexToast.Type.SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// LIVE UPDATERS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void startStatsUpdater() {
|
||||||
|
handler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isFinishing() || isDestroyed()) return;
|
||||||
|
|
||||||
|
int users = 1200 + random.nextInt(100);
|
||||||
|
int sessions = 3800 + random.nextInt(200);
|
||||||
|
int requests = 800 + random.nextInt(100);
|
||||||
|
|
||||||
|
statUsers.setText(String.format(Locale.US, "%,d", users));
|
||||||
|
statSessions.setText(String.format(Locale.US, "%,d", sessions));
|
||||||
|
statRequests.setText(String.valueOf(requests));
|
||||||
|
|
||||||
|
handler.postDelayed(this, 3000);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startNetworkUpdater() {
|
||||||
|
handler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isFinishing() || isDestroyed()) return;
|
||||||
|
|
||||||
|
float bandwidth = 8.0f + random.nextFloat() * 12.0f;
|
||||||
|
int latency = 10 + random.nextInt(50);
|
||||||
|
float packetLoss = random.nextFloat() * 0.1f;
|
||||||
|
|
||||||
|
netBandwidth.setText(String.format(Locale.US, "%.1f MB/s", bandwidth));
|
||||||
|
netLatency.setText(latency + "ms");
|
||||||
|
netPacketLoss.setText(String.format(Locale.US, "%.2f%%", packetLoss));
|
||||||
|
|
||||||
|
handler.postDelayed(this, 3000);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startLogUpdater() {
|
||||||
|
handler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isFinishing() || isDestroyed()) return;
|
||||||
|
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(AnalyticsActivity.this);
|
||||||
|
String entry = generateLogEntry();
|
||||||
|
addLogEntryView(entry, monoFont, true);
|
||||||
|
|
||||||
|
handler.postDelayed(this, 5000);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// HELPERS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void addSectionHeader(LinearLayout parent, String text, Typeface monoFont) {
|
||||||
|
TextView header = new TextView(this);
|
||||||
|
header.setText(text);
|
||||||
|
header.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
header.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
header.setTypeface(monoFont);
|
||||||
|
header.setLetterSpacing(0.15f);
|
||||||
|
LinearLayout.LayoutParams hParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
hParams.bottomMargin = dpToPx(12);
|
||||||
|
header.setLayoutParams(hParams);
|
||||||
|
parent.addView(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDivider(LinearLayout parent) {
|
||||||
|
View divider = new View(this);
|
||||||
|
LinearLayout.LayoutParams dParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(1));
|
||||||
|
dParams.topMargin = dpToPx(20);
|
||||||
|
dParams.bottomMargin = dpToPx(16);
|
||||||
|
divider.setLayoutParams(dParams);
|
||||||
|
divider.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
parent.addView(divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout createHorizontalRow(int bottomMargin) {
|
||||||
|
LinearLayout row = new LinearLayout(this);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rowParams.bottomMargin = bottomMargin;
|
||||||
|
row.setLayoutParams(rowParams);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// LIFECYCLE
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (handler != null) handler.removeCallbacksAndMessages(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
android/app/src/main/java/com/aethex/os/AppActivity.java
Normal file
66
android/app/src/main/java/com/aethex/os/AppActivity.java
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
public class AppActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
String appName = getIntent().getStringExtra("app_name");
|
||||||
|
String appId = getIntent().getStringExtra("app_id");
|
||||||
|
|
||||||
|
if (appName != null) {
|
||||||
|
TextView title = findViewById(R.id.app_title);
|
||||||
|
title.setText(appName);
|
||||||
|
TextView nameDisplay = findViewById(R.id.app_name_display);
|
||||||
|
nameDisplay.setText(appName);
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById(R.id.app_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Entrance animation for the content
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
254
android/app/src/main/java/com/aethex/os/AppAdapter.java
Normal file
254
android/app/src/main/java/com/aethex/os/AppAdapter.java
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.ColorMatrix;
|
||||||
|
import android.graphics.ColorMatrixColorFilter;
|
||||||
|
import android.graphics.Outline;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewOutlineProvider;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AppAdapter extends RecyclerView.Adapter<AppAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private List<AppInfo> appList;
|
||||||
|
private LayoutInflater inflater;
|
||||||
|
private Context context;
|
||||||
|
private ScreenTimeTracker screenTimeTracker;
|
||||||
|
private OnAppListChangedListener appListChangedListener;
|
||||||
|
|
||||||
|
// Track touch coordinates for context menu positioning
|
||||||
|
private float lastTouchX = 0f;
|
||||||
|
private float lastTouchY = 0f;
|
||||||
|
|
||||||
|
// Monochrome filter for third-party app icons
|
||||||
|
private static final ColorMatrixColorFilter MONO_FILTER;
|
||||||
|
static {
|
||||||
|
ColorMatrix matrix = new ColorMatrix();
|
||||||
|
matrix.setSaturation(0);
|
||||||
|
MONO_FILTER = new ColorMatrixColorFilter(matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnAppListChangedListener {
|
||||||
|
void onAppListChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppAdapter(Context context, List<AppInfo> appList) {
|
||||||
|
this.inflater = LayoutInflater.from(context);
|
||||||
|
this.context = context;
|
||||||
|
this.appList = appList;
|
||||||
|
this.screenTimeTracker = new ScreenTimeTracker(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnAppListChangedListener(OnAppListChangedListener listener) {
|
||||||
|
this.appListChangedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = inflater.inflate(R.layout.item_app_icon, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
AppInfo app = appList.get(position);
|
||||||
|
holder.appName.setText(app.getName());
|
||||||
|
|
||||||
|
// Set icon — drawable for real apps, resource for built-in
|
||||||
|
if (app.hasDrawableIcon()) {
|
||||||
|
holder.appIcon.setImageDrawable(app.getDrawableIcon());
|
||||||
|
holder.appIcon.setColorFilter(MONO_FILTER);
|
||||||
|
// Clip into uniform rounded square
|
||||||
|
holder.appIcon.setBackgroundResource(R.drawable.bg_app_icon_mask);
|
||||||
|
int pad = dpToPx(4);
|
||||||
|
holder.appIcon.setPadding(pad, pad, pad, pad);
|
||||||
|
holder.appIcon.setClipToOutline(true);
|
||||||
|
holder.appIcon.setOutlineProvider(new ViewOutlineProvider() {
|
||||||
|
@Override
|
||||||
|
public void getOutline(View view, Outline outline) {
|
||||||
|
float radius = dpToPx(10);
|
||||||
|
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
holder.appIcon.setImageResource(app.getIcon());
|
||||||
|
holder.appIcon.clearColorFilter();
|
||||||
|
holder.appIcon.setBackground(null);
|
||||||
|
holder.appIcon.setPadding(0, 0, 0, 0);
|
||||||
|
holder.appIcon.setClipToOutline(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track touch position for context menu placement
|
||||||
|
holder.itemView.setOnTouchListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
|
lastTouchX = event.getRawX();
|
||||||
|
lastTouchY = event.getRawY();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scale animation on click
|
||||||
|
holder.itemView.setOnClickListener(v -> {
|
||||||
|
v.animate().scaleX(0.9f).scaleY(0.9f).setDuration(80).withEndAction(() -> {
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(80).start();
|
||||||
|
screenTimeTracker.recordLaunch(app.getAppId());
|
||||||
|
launchApp(app);
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Long-press context menu with pin/hide options
|
||||||
|
holder.itemView.setOnLongClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
|
||||||
|
if (!(context instanceof Activity)) return false;
|
||||||
|
Activity activity = (Activity) context;
|
||||||
|
|
||||||
|
boolean pinned = screenTimeTracker.isPinned(app.getAppId());
|
||||||
|
String pinLabel = pinned ? "Unpin" : "Pin";
|
||||||
|
String pinEmoji = pinned ? "○" : "●";
|
||||||
|
|
||||||
|
AeThexContextMenu.MenuItem[] items;
|
||||||
|
if (app.isSystemApp()) {
|
||||||
|
items = new AeThexContextMenu.MenuItem[] {
|
||||||
|
new AeThexContextMenu.MenuItem("open", "Open", "▶"),
|
||||||
|
new AeThexContextMenu.MenuItem("pin", pinLabel, pinEmoji),
|
||||||
|
new AeThexContextMenu.MenuItem("info", "App Info", "ℹ"),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
items = new AeThexContextMenu.MenuItem[] {
|
||||||
|
new AeThexContextMenu.MenuItem("open", "Open", "▶"),
|
||||||
|
new AeThexContextMenu.MenuItem("pin", pinLabel, pinEmoji),
|
||||||
|
new AeThexContextMenu.MenuItem("hide", "Hide", "✕"),
|
||||||
|
new AeThexContextMenu.MenuItem("info", "App Info", "ℹ"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AeThexContextMenu.show(activity, lastTouchX, lastTouchY,
|
||||||
|
app.getName().toUpperCase(), items, actionId -> {
|
||||||
|
switch (actionId) {
|
||||||
|
case "open":
|
||||||
|
launchApp(app);
|
||||||
|
break;
|
||||||
|
case "pin":
|
||||||
|
boolean newPinState = !screenTimeTracker.isPinned(app.getAppId());
|
||||||
|
screenTimeTracker.setPinned(app.getAppId(), newPinState);
|
||||||
|
AeThexToast.show(activity,
|
||||||
|
app.getName() + (newPinState ? " pinned" : " unpinned"),
|
||||||
|
AeThexToast.Type.SUCCESS);
|
||||||
|
if (appListChangedListener != null) appListChangedListener.onAppListChanged();
|
||||||
|
break;
|
||||||
|
case "hide":
|
||||||
|
screenTimeTracker.setHidden(app.getAppId(), true);
|
||||||
|
AeThexToast.show(activity,
|
||||||
|
app.getName() + " hidden (restore in Settings)",
|
||||||
|
AeThexToast.Type.INFO);
|
||||||
|
if (appListChangedListener != null) appListChangedListener.onAppListChanged();
|
||||||
|
break;
|
||||||
|
case "info":
|
||||||
|
int launches = screenTimeTracker.getLaunchCount(app.getAppId());
|
||||||
|
String cat = screenTimeTracker.categorize(app.getAppId());
|
||||||
|
String info = app.getName() + " · " + launches + " launches · " + cat;
|
||||||
|
if (app.getPackageName() != null) {
|
||||||
|
info += "\n" + app.getPackageName();
|
||||||
|
}
|
||||||
|
AeThexToast.show(activity, info, AeThexToast.Type.INFO);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stagger fade-in animation for grid items
|
||||||
|
holder.itemView.setAlpha(0f);
|
||||||
|
holder.itemView.setTranslationY(20f);
|
||||||
|
holder.itemView.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.translationY(0f)
|
||||||
|
.setDuration(300)
|
||||||
|
.setStartDelay(position * 30L)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the app — either built-in AeThexOS activity or real Android app.
|
||||||
|
*/
|
||||||
|
private void launchApp(AppInfo app) {
|
||||||
|
// If it's a real Android app, launch via package manager
|
||||||
|
if (!app.isSystemApp() && app.getPackageName() != null) {
|
||||||
|
PackageManager pm = context.getPackageManager();
|
||||||
|
Intent launchIntent = pm.getLaunchIntentForPackage(app.getPackageName());
|
||||||
|
if (launchIntent != null) {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
context.startActivity(launchIntent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in AeThexOS app
|
||||||
|
Intent intent;
|
||||||
|
switch (app.getAppId()) {
|
||||||
|
case "settings": intent = new Intent(context, SettingsActivity.class); break;
|
||||||
|
case "calculator": intent = new Intent(context, CalculatorActivity.class); break;
|
||||||
|
case "terminal": intent = new Intent(context, TerminalActivity.class); break;
|
||||||
|
case "clock": intent = new Intent(context, ClockActivity.class); break;
|
||||||
|
case "snake": intent = new Intent(context, SnakeActivity.class); break;
|
||||||
|
case "notes": intent = new Intent(context, NotesActivity.class); break;
|
||||||
|
case "weather": intent = new Intent(context, WeatherActivity.class); break;
|
||||||
|
case "minesweeper": intent = new Intent(context, MinesweeperActivity.class); break;
|
||||||
|
case "files": intent = new Intent(context, FileManagerActivity.class); break;
|
||||||
|
case "music": intent = new Intent(context, MusicActivity.class); break;
|
||||||
|
case "chat": intent = new Intent(context, ChatActivity.class); break;
|
||||||
|
case "photos": intent = new Intent(context, PhotosActivity.class); break;
|
||||||
|
case "browser": intent = new Intent(context, BrowserActivity.class); break;
|
||||||
|
case "passport": intent = new Intent(context, PassportActivity.class); break;
|
||||||
|
case "projects": intent = new Intent(context, ProjectsActivity.class); break;
|
||||||
|
case "marketplace": intent = new Intent(context, MarketplaceActivity.class); break;
|
||||||
|
case "analytics": intent = new Intent(context, AnalyticsActivity.class); break;
|
||||||
|
case "mail": intent = new Intent(context, MailActivity.class); break;
|
||||||
|
case "camera": intent = new Intent(context, CameraActivity.class); break;
|
||||||
|
case "achievements": intent = new Intent(context, AchievementsActivity.class); break;
|
||||||
|
default:
|
||||||
|
intent = new Intent(context, AppActivity.class);
|
||||||
|
intent.putExtra("app_name", app.getName());
|
||||||
|
intent.putExtra("app_id", app.getAppId());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
context.startActivity(intent);
|
||||||
|
if (context instanceof Activity) {
|
||||||
|
((Activity) context).overridePendingTransition(R.anim.slide_up_in, R.anim.scale_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return appList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * context.getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView appName;
|
||||||
|
ImageView appIcon;
|
||||||
|
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
appName = itemView.findViewById(R.id.app_name_text_view);
|
||||||
|
appIcon = itemView.findViewById(R.id.app_icon_image_view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
android/app/src/main/java/com/aethex/os/AppInfo.java
Normal file
50
android/app/src/main/java/com/aethex/os/AppInfo.java
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
public class AppInfo {
|
||||||
|
private String name;
|
||||||
|
private int icon;
|
||||||
|
private String appId;
|
||||||
|
private Drawable drawableIcon; // For real Android app icons
|
||||||
|
private String packageName; // For real Android apps
|
||||||
|
private boolean isSystemApp; // AeThexOS built-in vs third-party
|
||||||
|
private boolean isPinned;
|
||||||
|
private boolean isHidden;
|
||||||
|
|
||||||
|
// Built-in AeThexOS app
|
||||||
|
public AppInfo(String name, int icon, String appId) {
|
||||||
|
this.name = name;
|
||||||
|
this.icon = icon;
|
||||||
|
this.appId = appId;
|
||||||
|
this.isSystemApp = true;
|
||||||
|
this.isPinned = false;
|
||||||
|
this.isHidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real Android app from PackageManager
|
||||||
|
public AppInfo(String name, Drawable drawableIcon, String appId, String packageName) {
|
||||||
|
this.name = name;
|
||||||
|
this.drawableIcon = drawableIcon;
|
||||||
|
this.appId = appId;
|
||||||
|
this.packageName = packageName;
|
||||||
|
this.icon = 0;
|
||||||
|
this.isSystemApp = false;
|
||||||
|
this.isPinned = false;
|
||||||
|
this.isHidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public int getIcon() { return icon; }
|
||||||
|
public String getAppId() { return appId; }
|
||||||
|
public Drawable getDrawableIcon() { return drawableIcon; }
|
||||||
|
public String getPackageName() { return packageName; }
|
||||||
|
public boolean isSystemApp() { return isSystemApp; }
|
||||||
|
public boolean hasDrawableIcon() { return drawableIcon != null; }
|
||||||
|
|
||||||
|
public boolean isPinned() { return isPinned; }
|
||||||
|
public void setPinned(boolean pinned) { this.isPinned = pinned; }
|
||||||
|
|
||||||
|
public boolean isHidden() { return isHidden; }
|
||||||
|
public void setHidden(boolean hidden) { this.isHidden = hidden; }
|
||||||
|
}
|
||||||
238
android/app/src/main/java/com/aethex/os/ArcadeActivity.java
Normal file
238
android/app/src/main/java/com/aethex/os/ArcadeActivity.java
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArcadeActivity - Retro game center for Pixel Arcade module.
|
||||||
|
* Launch classic games with AeThex styling.
|
||||||
|
*/
|
||||||
|
public class ArcadeActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private FrameLayout gameContainer;
|
||||||
|
private LinearLayout menuContainer;
|
||||||
|
private SnakeGame snakeGame;
|
||||||
|
private boolean inGame = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Root container
|
||||||
|
FrameLayout root = new FrameLayout(this);
|
||||||
|
root.setBackgroundColor(Color.parseColor("#0A0C14"));
|
||||||
|
|
||||||
|
// Menu container
|
||||||
|
menuContainer = new LinearLayout(this);
|
||||||
|
menuContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
menuContainer.setGravity(Gravity.CENTER);
|
||||||
|
menuContainer.setPadding(dpToPx(24), dpToPx(40), dpToPx(24), dpToPx(24));
|
||||||
|
|
||||||
|
// Game container (for fullscreen games)
|
||||||
|
gameContainer = new FrameLayout(this);
|
||||||
|
gameContainer.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
root.addView(menuContainer, new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
root.addView(gameContainer, new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
setContentView(root);
|
||||||
|
|
||||||
|
buildMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildMenu() {
|
||||||
|
menuContainer.removeAllViews();
|
||||||
|
|
||||||
|
Typeface displayFont = themeManager.getDisplayFont(this);
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(this);
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
TextView backBtn = new TextView(this);
|
||||||
|
backBtn.setText("← ARCADE");
|
||||||
|
backBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
backBtn.setTextColor(Color.parseColor("#88FFFFFF"));
|
||||||
|
backBtn.setTypeface(monoFont);
|
||||||
|
backBtn.setGravity(Gravity.START);
|
||||||
|
backBtn.setPadding(0, 0, 0, dpToPx(20));
|
||||||
|
backBtn.setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
menuContainer.addView(backBtn);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
TextView title = new TextView(this);
|
||||||
|
title.setText("PIXEL ARCADE");
|
||||||
|
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 32);
|
||||||
|
title.setTextColor(primaryColor);
|
||||||
|
title.setTypeface(displayFont, Typeface.BOLD);
|
||||||
|
title.setGravity(Gravity.CENTER);
|
||||||
|
title.setLetterSpacing(0.1f);
|
||||||
|
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
titleParams.bottomMargin = dpToPx(8);
|
||||||
|
titleParams.gravity = Gravity.CENTER;
|
||||||
|
title.setLayoutParams(titleParams);
|
||||||
|
menuContainer.addView(title);
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
TextView subtitle = new TextView(this);
|
||||||
|
subtitle.setText("Retro games for agents");
|
||||||
|
subtitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
subtitle.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
subtitle.setTypeface(monoFont);
|
||||||
|
subtitle.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams subParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
subParams.bottomMargin = dpToPx(40);
|
||||||
|
subParams.gravity = Gravity.CENTER;
|
||||||
|
subtitle.setLayoutParams(subParams);
|
||||||
|
menuContainer.addView(subtitle);
|
||||||
|
|
||||||
|
// Games list
|
||||||
|
menuContainer.addView(createGameCard("SNAKE", "Classic snake game\nSwipe to control, eat to grow",
|
||||||
|
"#22C55E", () -> launchSnake()));
|
||||||
|
menuContainer.addView(createGameCard("PONG", "Coming soon\nClassic paddle game",
|
||||||
|
"#66FFFFFF", null));
|
||||||
|
menuContainer.addView(createGameCard("TETRIS", "Coming soon\nBlock stacking puzzle",
|
||||||
|
"#66FFFFFF", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout createGameCard(String name, String desc, String accentColor, Runnable onLaunch) {
|
||||||
|
Typeface displayFont = themeManager.getDisplayFont(this);
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(this);
|
||||||
|
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
card.setPadding(dpToPx(18), dpToPx(16), dpToPx(18), dpToPx(16));
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(12));
|
||||||
|
bg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
int color = Color.parseColor(accentColor);
|
||||||
|
bg.setStroke(dpToPx(1), Color.argb(60, Color.red(color), Color.green(color), Color.blue(color)));
|
||||||
|
card.setBackground(bg);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
cardParams.bottomMargin = dpToPx(12);
|
||||||
|
card.setLayoutParams(cardParams);
|
||||||
|
|
||||||
|
// Name
|
||||||
|
TextView nameView = new TextView(this);
|
||||||
|
nameView.setText(name);
|
||||||
|
nameView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
|
||||||
|
nameView.setTextColor(Color.parseColor(accentColor));
|
||||||
|
nameView.setTypeface(displayFont, Typeface.BOLD);
|
||||||
|
card.addView(nameView);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
TextView descView = new TextView(this);
|
||||||
|
descView.setText(desc);
|
||||||
|
descView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
descView.setTextColor(Color.parseColor("#88FFFFFF"));
|
||||||
|
descView.setTypeface(monoFont);
|
||||||
|
descView.setPadding(0, dpToPx(4), 0, 0);
|
||||||
|
card.addView(descView);
|
||||||
|
|
||||||
|
if (onLaunch != null) {
|
||||||
|
card.setOnClickListener(v -> {
|
||||||
|
v.animate().scaleX(0.97f).scaleY(0.97f).setDuration(80).withEndAction(() -> {
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(80).withEndAction(onLaunch).start();
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
card.setAlpha(0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void launchSnake() {
|
||||||
|
inGame = true;
|
||||||
|
menuContainer.setVisibility(View.GONE);
|
||||||
|
gameContainer.setVisibility(View.VISIBLE);
|
||||||
|
gameContainer.removeAllViews();
|
||||||
|
|
||||||
|
snakeGame = new SnakeGame(this);
|
||||||
|
gameContainer.addView(snakeGame, new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (inGame) {
|
||||||
|
inGame = false;
|
||||||
|
if (snakeGame != null) {
|
||||||
|
snakeGame.pause();
|
||||||
|
snakeGame = null;
|
||||||
|
}
|
||||||
|
gameContainer.removeAllViews();
|
||||||
|
gameContainer.setVisibility(View.GONE);
|
||||||
|
menuContainer.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if (snakeGame != null) snakeGame.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (snakeGame != null) snakeGame.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
216
android/app/src/main/java/com/aethex/os/BottomNavBar.java
Normal file
216
android/app/src/main/java/com/aethex/os/BottomNavBar.java
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom Navigation Bar - Reusable component for AeThexOS apps.
|
||||||
|
* Shows Home, Projects, Chat, Marketplace, Settings
|
||||||
|
*/
|
||||||
|
public class BottomNavBar {
|
||||||
|
|
||||||
|
public static final String TAB_HOME = "home";
|
||||||
|
public static final String TAB_PROJECTS = "projects";
|
||||||
|
public static final String TAB_CHAT = "chat";
|
||||||
|
public static final String TAB_MARKETPLACE = "marketplace";
|
||||||
|
public static final String TAB_SETTINGS = "settings";
|
||||||
|
|
||||||
|
private static final int NAV_HEIGHT_DP = 60;
|
||||||
|
|
||||||
|
public interface OnTabSelectedListener {
|
||||||
|
void onTabSelected(String tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and attaches a bottom navigation bar to the activity.
|
||||||
|
*
|
||||||
|
* @param activity The activity to attach to
|
||||||
|
* @param rootLayout The root FrameLayout or parent ViewGroup
|
||||||
|
* @param activeTab Which tab is currently active (use TAB_* constants)
|
||||||
|
* @param listener Optional callback for tab selection
|
||||||
|
* @return The created nav bar view
|
||||||
|
*/
|
||||||
|
public static View attach(Activity activity, ViewGroup rootLayout, String activeTab, OnTabSelectedListener listener) {
|
||||||
|
Context context = activity;
|
||||||
|
ThemeManager themeManager = new ThemeManager(context);
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(context);
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(context);
|
||||||
|
|
||||||
|
// Create the nav bar container
|
||||||
|
LinearLayout navBar = new LinearLayout(context);
|
||||||
|
navBar.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
navBar.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
// Glassmorphic background
|
||||||
|
GradientDrawable navBg = new GradientDrawable();
|
||||||
|
navBg.setColor(Color.parseColor("#E6000000")); // 90% black
|
||||||
|
navBg.setCornerRadii(new float[]{
|
||||||
|
dpToPx(context, 16), dpToPx(context, 16), // top corners
|
||||||
|
dpToPx(context, 16), dpToPx(context, 16),
|
||||||
|
0, 0, 0, 0 // bottom corners
|
||||||
|
});
|
||||||
|
navBg.setStroke(dpToPx(context, 1), Color.parseColor("#33FFFFFF"));
|
||||||
|
navBar.setBackground(navBg);
|
||||||
|
navBar.setElevation(dpToPx(context, 8));
|
||||||
|
|
||||||
|
// Layout params - fixed at bottom
|
||||||
|
FrameLayout.LayoutParams navParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
dpToPx(context, NAV_HEIGHT_DP)
|
||||||
|
);
|
||||||
|
navParams.gravity = Gravity.BOTTOM;
|
||||||
|
navBar.setLayoutParams(navParams);
|
||||||
|
|
||||||
|
// Add tabs
|
||||||
|
String[][] tabs = {
|
||||||
|
{TAB_HOME, "Home", "⌂"},
|
||||||
|
{TAB_PROJECTS, "Projects", "◫"},
|
||||||
|
{TAB_CHAT, "Chat", "✉"},
|
||||||
|
{TAB_MARKETPLACE, "Market", "◈"},
|
||||||
|
{TAB_SETTINGS, "Settings", "⚙"},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String[] tab : tabs) {
|
||||||
|
View tabView = createTab(context, tab[0], tab[1], tab[2],
|
||||||
|
tab[0].equals(activeTab), primaryColor, monoFont, () -> {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onTabSelected(tab[0]);
|
||||||
|
} else {
|
||||||
|
navigateTo(activity, tab[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
navBar.addView(tabView);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to root layout
|
||||||
|
rootLayout.addView(navBar);
|
||||||
|
|
||||||
|
return navBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified attach without listener - uses default navigation
|
||||||
|
*/
|
||||||
|
public static View attach(Activity activity, ViewGroup rootLayout, String activeTab) {
|
||||||
|
return attach(activity, rootLayout, activeTab, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static View createTab(Context context, String id, String label, String icon,
|
||||||
|
boolean isActive, int primaryColor, Typeface font, Runnable onClick) {
|
||||||
|
LinearLayout tab = new LinearLayout(context);
|
||||||
|
tab.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
tab.setGravity(Gravity.CENTER);
|
||||||
|
tab.setPadding(dpToPx(context, 8), dpToPx(context, 8), dpToPx(context, 8), dpToPx(context, 8));
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams tabParams = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f
|
||||||
|
);
|
||||||
|
tab.setLayoutParams(tabParams);
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
TextView iconView = new TextView(context);
|
||||||
|
iconView.setText(icon);
|
||||||
|
iconView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||||
|
iconView.setGravity(Gravity.CENTER);
|
||||||
|
if (isActive) {
|
||||||
|
iconView.setTextColor(primaryColor);
|
||||||
|
} else {
|
||||||
|
iconView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
}
|
||||||
|
tab.addView(iconView);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
TextView labelView = new TextView(context);
|
||||||
|
labelView.setText(label);
|
||||||
|
labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
labelView.setTypeface(font);
|
||||||
|
labelView.setGravity(Gravity.CENTER);
|
||||||
|
labelView.setLetterSpacing(0.05f);
|
||||||
|
if (isActive) {
|
||||||
|
labelView.setTextColor(primaryColor);
|
||||||
|
} else {
|
||||||
|
labelView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
}
|
||||||
|
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
labelParams.topMargin = dpToPx(context, 2);
|
||||||
|
labelView.setLayoutParams(labelParams);
|
||||||
|
tab.addView(labelView);
|
||||||
|
|
||||||
|
// Active indicator dot
|
||||||
|
if (isActive) {
|
||||||
|
View dot = new View(context);
|
||||||
|
GradientDrawable dotBg = new GradientDrawable();
|
||||||
|
dotBg.setShape(GradientDrawable.OVAL);
|
||||||
|
dotBg.setColor(primaryColor);
|
||||||
|
dot.setBackground(dotBg);
|
||||||
|
LinearLayout.LayoutParams dotParams = new LinearLayout.LayoutParams(
|
||||||
|
dpToPx(context, 4), dpToPx(context, 4)
|
||||||
|
);
|
||||||
|
dotParams.topMargin = dpToPx(context, 4);
|
||||||
|
dot.setLayoutParams(dotParams);
|
||||||
|
tab.addView(dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click handler
|
||||||
|
tab.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
onClick.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void navigateTo(Activity activity, String tabId) {
|
||||||
|
Class<?> targetClass;
|
||||||
|
|
||||||
|
switch (tabId) {
|
||||||
|
case TAB_HOME:
|
||||||
|
targetClass = SystemActivity.class;
|
||||||
|
break;
|
||||||
|
case TAB_PROJECTS:
|
||||||
|
targetClass = ProjectsActivity.class;
|
||||||
|
break;
|
||||||
|
case TAB_CHAT:
|
||||||
|
targetClass = ChatActivity.class;
|
||||||
|
break;
|
||||||
|
case TAB_MARKETPLACE:
|
||||||
|
targetClass = MarketplaceActivity.class;
|
||||||
|
break;
|
||||||
|
case TAB_SETTINGS:
|
||||||
|
targetClass = SettingsActivity.class;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't navigate if already on this activity
|
||||||
|
if (activity.getClass().equals(targetClass)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent = new Intent(activity, targetClass);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||||
|
activity.startActivity(intent);
|
||||||
|
activity.overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int dpToPx(Context context, int dp) {
|
||||||
|
return (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
978
android/app/src/main/java/com/aethex/os/BrowserActivity.java
Normal file
978
android/app/src/main/java/com/aethex/os/BrowserActivity.java
Normal file
|
|
@ -0,0 +1,978 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.webkit.WebChromeClient;
|
||||||
|
import android.webkit.WebResourceRequest;
|
||||||
|
import android.webkit.WebSettings;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BrowserActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private EditText urlBar;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
private View divider;
|
||||||
|
private LinearLayout tabStrip;
|
||||||
|
private TextView navBack, navForward, securityIcon, bookmarkBtn;
|
||||||
|
private FrameLayout webViewContainer;
|
||||||
|
|
||||||
|
// Find-in-page
|
||||||
|
private LinearLayout findBar;
|
||||||
|
private EditText findInput;
|
||||||
|
private TextView findCount, findPrev, findNext, findClose;
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
private List<BrowserTab> tabs = new ArrayList<>();
|
||||||
|
private int activeTabIndex = -1;
|
||||||
|
|
||||||
|
// Bookmarks / History
|
||||||
|
private static final String PREFS_BROWSER = "aethex_browser";
|
||||||
|
private static final String KEY_BOOKMARKS = "bookmarks";
|
||||||
|
private static final String KEY_HISTORY = "history";
|
||||||
|
|
||||||
|
private static class BrowserTab {
|
||||||
|
WebView webView;
|
||||||
|
String title;
|
||||||
|
String url;
|
||||||
|
boolean isHome;
|
||||||
|
|
||||||
|
BrowserTab(WebView wv) {
|
||||||
|
this.webView = wv;
|
||||||
|
this.title = "New Tab";
|
||||||
|
this.url = "";
|
||||||
|
this.isHome = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_browser);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Bind views
|
||||||
|
urlBar = findViewById(R.id.browser_url_bar);
|
||||||
|
progressBar = findViewById(R.id.browser_progress);
|
||||||
|
divider = findViewById(R.id.browser_divider);
|
||||||
|
tabStrip = findViewById(R.id.browser_tab_strip);
|
||||||
|
navBack = findViewById(R.id.browser_nav_back);
|
||||||
|
navForward = findViewById(R.id.browser_nav_forward);
|
||||||
|
securityIcon = findViewById(R.id.browser_security_icon);
|
||||||
|
bookmarkBtn = findViewById(R.id.browser_bookmark_btn);
|
||||||
|
webViewContainer = findViewById(R.id.browser_webview_container);
|
||||||
|
|
||||||
|
// Find-in-page
|
||||||
|
findBar = findViewById(R.id.browser_find_bar);
|
||||||
|
findInput = findViewById(R.id.browser_find_input);
|
||||||
|
findCount = findViewById(R.id.browser_find_count);
|
||||||
|
findPrev = findViewById(R.id.browser_find_prev);
|
||||||
|
findNext = findViewById(R.id.browser_find_next);
|
||||||
|
findClose = findViewById(R.id.browser_find_close);
|
||||||
|
|
||||||
|
// Remove the XML WebView — we manage WebViews per tab
|
||||||
|
WebView xmlWebView = findViewById(R.id.browser_webview);
|
||||||
|
if (xmlWebView != null) {
|
||||||
|
webViewContainer.removeView(xmlWebView);
|
||||||
|
xmlWebView.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
findViewById(R.id.browser_close_btn).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
navBack.setOnClickListener(v -> {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null && tab.webView.canGoBack()) {
|
||||||
|
tab.webView.goBack();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
navForward.setOnClickListener(v -> {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null && tab.webView.canGoForward()) {
|
||||||
|
tab.webView.goForward();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh
|
||||||
|
findViewById(R.id.browser_refresh_btn).setOnClickListener(v -> {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) {
|
||||||
|
if (tab.isHome) {
|
||||||
|
loadHomePage(tab);
|
||||||
|
} else {
|
||||||
|
tab.webView.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// URL bar
|
||||||
|
urlBar.setSelectAllOnFocus(true);
|
||||||
|
urlBar.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_GO ||
|
||||||
|
(event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||||
|
&& event.getAction() == KeyEvent.ACTION_DOWN)) {
|
||||||
|
navigateToInput();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bookmark toggle
|
||||||
|
bookmarkBtn.setOnClickListener(v -> toggleBookmark());
|
||||||
|
|
||||||
|
// Menu button
|
||||||
|
findViewById(R.id.browser_menu_btn).setOnClickListener(v -> showBrowserMenu());
|
||||||
|
|
||||||
|
// New tab button
|
||||||
|
findViewById(R.id.browser_new_tab_btn).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
addNewTab(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find-in-page controls
|
||||||
|
setupFindInPage();
|
||||||
|
|
||||||
|
// Open first tab
|
||||||
|
String intentUrl = getIntent().getStringExtra("url");
|
||||||
|
addNewTab(intentUrl);
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View root = findViewById(R.id.browser_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
AeThexKeyboard.attach(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Tab Management
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private void addNewTab(String url) {
|
||||||
|
WebView wv = createWebView();
|
||||||
|
BrowserTab tab = new BrowserTab(wv);
|
||||||
|
tabs.add(tab);
|
||||||
|
|
||||||
|
// Add to container but hide (will be shown by switchToTab)
|
||||||
|
wv.setVisibility(View.GONE);
|
||||||
|
// Insert before the find bar
|
||||||
|
webViewContainer.addView(wv, 0, new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
switchToTab(tabs.size() - 1);
|
||||||
|
|
||||||
|
if (url != null && !url.isEmpty()) {
|
||||||
|
navigateTab(tab, url);
|
||||||
|
} else {
|
||||||
|
loadHomePage(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void switchToTab(int index) {
|
||||||
|
if (index < 0 || index >= tabs.size()) return;
|
||||||
|
|
||||||
|
// Hide current
|
||||||
|
if (activeTabIndex >= 0 && activeTabIndex < tabs.size()) {
|
||||||
|
tabs.get(activeTabIndex).webView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTabIndex = index;
|
||||||
|
BrowserTab tab = tabs.get(index);
|
||||||
|
tab.webView.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
// Update URL bar
|
||||||
|
if (tab.isHome) {
|
||||||
|
urlBar.setText("");
|
||||||
|
urlBar.setHint("Search or enter URL");
|
||||||
|
securityIcon.setText("Æ");
|
||||||
|
securityIcon.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
} else {
|
||||||
|
urlBar.setText(tab.url);
|
||||||
|
updateSecurityIcon(tab.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNavButtons();
|
||||||
|
updateBookmarkButton();
|
||||||
|
rebuildTabStrip();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeTab(int index) {
|
||||||
|
if (tabs.size() <= 1) {
|
||||||
|
// Last tab — just go home
|
||||||
|
loadHomePage(tabs.get(0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BrowserTab tab = tabs.get(index);
|
||||||
|
webViewContainer.removeView(tab.webView);
|
||||||
|
tab.webView.stopLoading();
|
||||||
|
tab.webView.destroy();
|
||||||
|
tabs.remove(index);
|
||||||
|
|
||||||
|
if (activeTabIndex >= tabs.size()) {
|
||||||
|
activeTabIndex = tabs.size() - 1;
|
||||||
|
} else if (activeTabIndex > index) {
|
||||||
|
activeTabIndex--;
|
||||||
|
} else if (activeTabIndex == index) {
|
||||||
|
activeTabIndex = Math.min(index, tabs.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToTab(activeTabIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BrowserTab activeTab() {
|
||||||
|
if (activeTabIndex >= 0 && activeTabIndex < tabs.size()) {
|
||||||
|
return tabs.get(activeTabIndex);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildTabStrip() {
|
||||||
|
tabStrip.removeAllViews();
|
||||||
|
Typeface mono = ResourcesCompat.getFont(this, R.font.source_code_pro);
|
||||||
|
int primary = themeManager.getPrimaryColor(this);
|
||||||
|
|
||||||
|
for (int i = 0; i < tabs.size(); i++) {
|
||||||
|
final int tabIndex = i;
|
||||||
|
BrowserTab tab = tabs.get(i);
|
||||||
|
boolean isActive = (i == activeTabIndex);
|
||||||
|
|
||||||
|
LinearLayout tabView = new LinearLayout(this);
|
||||||
|
tabView.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
tabView.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams tlp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, dpToPx(28));
|
||||||
|
tlp.setMarginEnd(dpToPx(2));
|
||||||
|
tabView.setLayoutParams(tlp);
|
||||||
|
tabView.setPadding(dpToPx(10), 0, dpToPx(4), 0);
|
||||||
|
|
||||||
|
GradientDrawable tabBg = new GradientDrawable();
|
||||||
|
tabBg.setCornerRadii(new float[]{
|
||||||
|
dpToPx(8), dpToPx(8), dpToPx(8), dpToPx(8), 0, 0, 0, 0});
|
||||||
|
if (isActive) {
|
||||||
|
tabBg.setColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
tabBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
} else {
|
||||||
|
tabBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
}
|
||||||
|
tabView.setBackground(tabBg);
|
||||||
|
|
||||||
|
// Tab title
|
||||||
|
TextView title = new TextView(this);
|
||||||
|
String displayTitle = tab.title;
|
||||||
|
if (displayTitle.length() > 14) displayTitle = displayTitle.substring(0, 14) + "…";
|
||||||
|
title.setText(displayTitle);
|
||||||
|
title.setTextSize(10f);
|
||||||
|
title.setTextColor(isActive ? Color.parseColor("#E0FFFFFF") : Color.parseColor("#66FFFFFF"));
|
||||||
|
title.setTypeface(mono);
|
||||||
|
title.setSingleLine(true);
|
||||||
|
tabView.addView(title);
|
||||||
|
|
||||||
|
// Close X on tab
|
||||||
|
TextView closeX = new TextView(this);
|
||||||
|
closeX.setText("×");
|
||||||
|
closeX.setTextSize(14f);
|
||||||
|
closeX.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
closeX.setPadding(dpToPx(6), 0, dpToPx(2), 0);
|
||||||
|
closeX.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
closeTab(tabIndex);
|
||||||
|
});
|
||||||
|
tabView.addView(closeX);
|
||||||
|
|
||||||
|
tabView.setOnClickListener(v -> {
|
||||||
|
if (tabIndex != activeTabIndex) {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
switchToTab(tabIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tabStrip.addView(tabView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// WebView Factory
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private WebView createWebView() {
|
||||||
|
WebView wv = new WebView(this);
|
||||||
|
WebSettings settings = wv.getSettings();
|
||||||
|
settings.setJavaScriptEnabled(true);
|
||||||
|
settings.setDomStorageEnabled(true);
|
||||||
|
settings.setBuiltInZoomControls(true);
|
||||||
|
settings.setDisplayZoomControls(false);
|
||||||
|
settings.setUseWideViewPort(true);
|
||||||
|
settings.setLoadWithOverviewMode(true);
|
||||||
|
settings.setSupportMultipleWindows(false);
|
||||||
|
settings.setAllowFileAccess(false);
|
||||||
|
settings.setAllowContentAccess(false);
|
||||||
|
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
|
||||||
|
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
|
||||||
|
String ua = settings.getUserAgentString();
|
||||||
|
settings.setUserAgentString(ua + " AeThexBrowser/2.0");
|
||||||
|
|
||||||
|
wv.setWebViewClient(new WebViewClient() {
|
||||||
|
@Override
|
||||||
|
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||||
|
super.onPageStarted(view, url, favicon);
|
||||||
|
BrowserTab tab = findTab(view);
|
||||||
|
if (tab != null && url != null && !url.startsWith("data:")) {
|
||||||
|
tab.url = url;
|
||||||
|
tab.isHome = false;
|
||||||
|
if (view == activeWebView()) {
|
||||||
|
urlBar.setText(url);
|
||||||
|
updateSecurityIcon(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateNavButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
BrowserTab tab = findTab(view);
|
||||||
|
if (tab != null) {
|
||||||
|
if (url != null && !url.startsWith("data:")) {
|
||||||
|
tab.url = url;
|
||||||
|
tab.title = view.getTitle() != null ? view.getTitle() : url;
|
||||||
|
tab.isHome = false;
|
||||||
|
addToHistory(url, tab.title);
|
||||||
|
}
|
||||||
|
if (view == activeWebView()) {
|
||||||
|
if (url != null && url.startsWith("data:")) {
|
||||||
|
urlBar.setText("");
|
||||||
|
urlBar.setHint("Search or enter URL");
|
||||||
|
} else {
|
||||||
|
urlBar.setText(url);
|
||||||
|
}
|
||||||
|
updateBookmarkButton();
|
||||||
|
}
|
||||||
|
rebuildTabStrip();
|
||||||
|
}
|
||||||
|
updateNavButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||||
|
super.onReceivedError(view, errorCode, description, failingUrl);
|
||||||
|
int primary = themeManager.getPrimaryColor(BrowserActivity.this);
|
||||||
|
String primaryHex = String.format("#%06X", (0xFFFFFF & primary));
|
||||||
|
String errorHtml = "<html><body style='background:#0A0E1A;color:#ff6b6b;font-family:monospace;padding:40px;text-align:center;'>"
|
||||||
|
+ "<div style='font-size:48px;margin-bottom:16px;'>⚠</div>"
|
||||||
|
+ "<h2 style='color:" + primaryHex + ";font-size:16px;'>CONNECTION FAILED</h2>"
|
||||||
|
+ "<p style='color:rgba(255,255,255,0.5);font-size:13px;margin-top:12px;'>Error " + errorCode + ": " + description + "</p>"
|
||||||
|
+ "<p style='color:rgba(255,255,255,0.2);font-size:11px;margin-top:16px;word-break:break-all;'>" + failingUrl + "</p>"
|
||||||
|
+ "</body></html>";
|
||||||
|
view.loadDataWithBaseURL(null, errorHtml, "text/html", "UTF-8", null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wv.setWebChromeClient(new WebChromeClient() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(WebView view, int newProgress) {
|
||||||
|
if (view == activeWebView()) {
|
||||||
|
if (newProgress < 100) {
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
divider.setVisibility(View.GONE);
|
||||||
|
progressBar.setProgress(newProgress);
|
||||||
|
} else {
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
divider.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedTitle(WebView view, String title) {
|
||||||
|
BrowserTab tab = findTab(view);
|
||||||
|
if (tab != null && title != null) {
|
||||||
|
tab.title = title;
|
||||||
|
rebuildTabStrip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wv.setBackgroundColor(0xFF0A0E1A);
|
||||||
|
return wv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BrowserTab findTab(WebView wv) {
|
||||||
|
for (BrowserTab t : tabs) {
|
||||||
|
if (t.webView == wv) return t;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebView activeWebView() {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
return tab != null ? tab.webView : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Navigation
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private void navigateToInput() {
|
||||||
|
String input = urlBar.getText().toString().trim();
|
||||||
|
if (input.isEmpty()) return;
|
||||||
|
|
||||||
|
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
|
||||||
|
if (imm != null) imm.hideSoftInputFromWindow(urlBar.getWindowToken(), 0);
|
||||||
|
urlBar.clearFocus();
|
||||||
|
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) {
|
||||||
|
navigateTab(tab, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void navigateTab(BrowserTab tab, String input) {
|
||||||
|
String url;
|
||||||
|
if (input.contains(".") && !input.contains(" ")) {
|
||||||
|
if (!input.startsWith("http://") && !input.startsWith("https://")) {
|
||||||
|
url = "https://" + input;
|
||||||
|
} else {
|
||||||
|
url = input;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url = "https://www.google.com/search?q=" + android.net.Uri.encode(input);
|
||||||
|
}
|
||||||
|
tab.isHome = false;
|
||||||
|
tab.url = url;
|
||||||
|
tab.webView.loadUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNavButtons() {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) {
|
||||||
|
navBack.setTextColor(tab.webView.canGoBack()
|
||||||
|
? Color.parseColor("#CCFFFFFF") : Color.parseColor("#33FFFFFF"));
|
||||||
|
navForward.setTextColor(tab.webView.canGoForward()
|
||||||
|
? Color.parseColor("#CCFFFFFF") : Color.parseColor("#33FFFFFF"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSecurityIcon(String url) {
|
||||||
|
if (url == null || url.startsWith("data:")) {
|
||||||
|
securityIcon.setText("Æ");
|
||||||
|
securityIcon.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
} else if (url.startsWith("https://")) {
|
||||||
|
securityIcon.setText("🔒");
|
||||||
|
securityIcon.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
} else {
|
||||||
|
securityIcon.setText("⚠");
|
||||||
|
securityIcon.setTextColor(Color.parseColor("#EF4444"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Home Page
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private void loadHomePage(BrowserTab tab) {
|
||||||
|
tab.title = "New Tab";
|
||||||
|
tab.url = "";
|
||||||
|
tab.isHome = true;
|
||||||
|
|
||||||
|
if (tab == activeTab()) {
|
||||||
|
urlBar.setText("");
|
||||||
|
urlBar.setHint("Search or enter URL");
|
||||||
|
securityIcon.setText("Æ");
|
||||||
|
securityIcon.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
updateBookmarkButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
int primaryInt = themeManager.getPrimaryColor(this);
|
||||||
|
String ph = String.format("#%06X", (0xFFFFFF & primaryInt));
|
||||||
|
|
||||||
|
// Build bookmarks grid HTML
|
||||||
|
List<String[]> bookmarks = getBookmarks();
|
||||||
|
StringBuilder bmHtml = new StringBuilder();
|
||||||
|
for (String[] bm : bookmarks) {
|
||||||
|
String initial = bm[0].length() > 0 ? bm[0].substring(0, 1).toUpperCase() : "?";
|
||||||
|
bmHtml.append("<a class='qlink' href='").append(bm[1]).append("'>")
|
||||||
|
.append("<div class='ql-icon'>").append(initial).append("</div>")
|
||||||
|
.append("<div class='ql-label'>").append(escapeHtml(bm[0])).append("</div>")
|
||||||
|
.append("</a>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no bookmarks, add defaults
|
||||||
|
if (bookmarks.isEmpty()) {
|
||||||
|
bmHtml.append("<a class='qlink' href='https://www.google.com'><div class='ql-icon'>G</div><div class='ql-label'>Google</div></a>");
|
||||||
|
bmHtml.append("<a class='qlink' href='https://github.com'><div class='ql-icon'>⬡</div><div class='ql-label'>GitHub</div></a>");
|
||||||
|
bmHtml.append("<a class='qlink' href='https://wikipedia.org'><div class='ql-icon'>W</div><div class='ql-label'>Wikipedia</div></a>");
|
||||||
|
bmHtml.append("<a class='qlink' href='https://youtube.com'><div class='ql-icon'>▶</div><div class='ql-label'>YouTube</div></a>");
|
||||||
|
bmHtml.append("<a class='qlink' href='https://reddit.com'><div class='ql-icon'>R</div><div class='ql-label'>Reddit</div></a>");
|
||||||
|
bmHtml.append("<a class='qlink' href='https://twitter.com'><div class='ql-icon'>X</div><div class='ql-label'>X / Twitter</div></a>");
|
||||||
|
}
|
||||||
|
|
||||||
|
String html = "<!DOCTYPE html><html><head>"
|
||||||
|
+ "<meta name='viewport' content='width=device-width,initial-scale=1.0'>"
|
||||||
|
+ "<style>"
|
||||||
|
+ "*{margin:0;padding:0;box-sizing:border-box}"
|
||||||
|
+ "body{background:#0A0E1A;color:rgba(255,255,255,0.8);font-family:'Courier New',monospace;"
|
||||||
|
+ "display:flex;flex-direction:column;align-items:center;min-height:100vh;padding:48px 20px 40px}"
|
||||||
|
+ ".brand{font-size:32px;font-weight:200;color:" + ph + ";letter-spacing:4px;margin-bottom:4px}"
|
||||||
|
+ ".sub{font-size:10px;color:rgba(255,255,255,0.3);letter-spacing:3px;margin-bottom:32px;text-transform:uppercase}"
|
||||||
|
+ ".search-wrap{width:100%;max-width:480px;margin-bottom:40px}"
|
||||||
|
+ ".search-wrap input{width:100%;padding:14px 18px;border-radius:12px;"
|
||||||
|
+ "border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.04);"
|
||||||
|
+ "color:rgba(255,255,255,0.8);font-size:14px;font-family:'Courier New',monospace;outline:none;"
|
||||||
|
+ "transition:border-color 0.2s}"
|
||||||
|
+ ".search-wrap input:focus{border-color:" + ph + "40}"
|
||||||
|
+ ".search-wrap input::placeholder{color:rgba(255,255,255,0.25)}"
|
||||||
|
+ ".sec-label{font-size:9px;color:rgba(255,255,255,0.25);letter-spacing:2px;text-transform:uppercase;"
|
||||||
|
+ "margin-bottom:14px;width:100%;max-width:480px}"
|
||||||
|
+ ".grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;width:100%;max-width:480px}"
|
||||||
|
+ ".qlink{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.05);"
|
||||||
|
+ "border-radius:12px;padding:16px 8px;text-align:center;text-decoration:none;"
|
||||||
|
+ "transition:all 0.15s}"
|
||||||
|
+ ".qlink:active{background:" + ph + "15;border-color:" + ph + "30}"
|
||||||
|
+ ".ql-icon{font-size:20px;color:" + ph + ";margin-bottom:6px}"
|
||||||
|
+ ".ql-label{font-size:10px;color:rgba(255,255,255,0.5);font-family:'Courier New',monospace;"
|
||||||
|
+ "white-space:nowrap;overflow:hidden;text-overflow:ellipsis}"
|
||||||
|
+ ".footer{margin-top:auto;padding-top:32px;font-size:9px;color:rgba(255,255,255,0.15);"
|
||||||
|
+ "letter-spacing:1px}"
|
||||||
|
+ "</style></head><body>"
|
||||||
|
+ "<div class='brand'>Æ</div>"
|
||||||
|
+ "<div class='sub'>AeThex Browser</div>"
|
||||||
|
+ "<div class='search-wrap'>"
|
||||||
|
+ "<form onsubmit=\"doSearch(event)\">"
|
||||||
|
+ "<input id='si' type='text' placeholder='Search the web...' autocomplete='off'>"
|
||||||
|
+ "</form></div>"
|
||||||
|
+ "<div class='sec-label'>QUICK ACCESS</div>"
|
||||||
|
+ "<div class='grid'>" + bmHtml.toString() + "</div>"
|
||||||
|
+ "<div class='footer'>AeThex Corporation · Secure Connection</div>"
|
||||||
|
+ "<script>"
|
||||||
|
+ "function doSearch(e){e.preventDefault();var q=document.getElementById('si').value.trim();"
|
||||||
|
+ "if(q)window.location.href='https://www.google.com/search?q='+encodeURIComponent(q)}"
|
||||||
|
+ "</script></body></html>";
|
||||||
|
|
||||||
|
tab.webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
|
||||||
|
rebuildTabStrip();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeHtml(String s) {
|
||||||
|
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
.replace("\"", """).replace("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Bookmarks
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private List<String[]> getBookmarks() {
|
||||||
|
SharedPreferences prefs = getSharedPreferences(PREFS_BROWSER, MODE_PRIVATE);
|
||||||
|
String json = prefs.getString(KEY_BOOKMARKS, "[]");
|
||||||
|
List<String[]> list = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
JSONArray arr = new JSONArray(json);
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONObject obj = arr.getJSONObject(i);
|
||||||
|
list.add(new String[]{obj.getString("title"), obj.getString("url")});
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveBookmarks(List<String[]> bookmarks) {
|
||||||
|
try {
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
for (String[] bm : bookmarks) {
|
||||||
|
JSONObject obj = new JSONObject();
|
||||||
|
obj.put("title", bm[0]);
|
||||||
|
obj.put("url", bm[1]);
|
||||||
|
arr.put(obj);
|
||||||
|
}
|
||||||
|
getSharedPreferences(PREFS_BROWSER, MODE_PRIVATE)
|
||||||
|
.edit().putString(KEY_BOOKMARKS, arr.toString()).apply();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBookmarked(String url) {
|
||||||
|
if (url == null || url.isEmpty()) return false;
|
||||||
|
for (String[] bm : getBookmarks()) {
|
||||||
|
if (bm[1].equals(url)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleBookmark() {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab == null || tab.isHome || tab.url.isEmpty()) return;
|
||||||
|
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
List<String[]> bookmarks = getBookmarks();
|
||||||
|
|
||||||
|
if (isBookmarked(tab.url)) {
|
||||||
|
// Remove
|
||||||
|
bookmarks.removeIf(bm -> bm[1].equals(tab.url));
|
||||||
|
saveBookmarks(bookmarks);
|
||||||
|
AeThexToast.show(this, "Bookmark removed", AeThexToast.Type.INFO);
|
||||||
|
} else {
|
||||||
|
// Add
|
||||||
|
bookmarks.add(0, new String[]{tab.title, tab.url});
|
||||||
|
saveBookmarks(bookmarks);
|
||||||
|
AeThexToast.show(this, "Bookmarked", AeThexToast.Type.SUCCESS);
|
||||||
|
}
|
||||||
|
updateBookmarkButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBookmarkButton() {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab == null || tab.isHome) {
|
||||||
|
bookmarkBtn.setText("☆");
|
||||||
|
bookmarkBtn.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isBookmarked(tab.url)) {
|
||||||
|
bookmarkBtn.setText("★");
|
||||||
|
bookmarkBtn.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
} else {
|
||||||
|
bookmarkBtn.setText("☆");
|
||||||
|
bookmarkBtn.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// History
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private void addToHistory(String url, String title) {
|
||||||
|
if (url == null || url.startsWith("data:") || url.isEmpty()) return;
|
||||||
|
try {
|
||||||
|
SharedPreferences prefs = getSharedPreferences(PREFS_BROWSER, MODE_PRIVATE);
|
||||||
|
String json = prefs.getString(KEY_HISTORY, "[]");
|
||||||
|
JSONArray arr = new JSONArray(json);
|
||||||
|
|
||||||
|
// Remove duplicate
|
||||||
|
JSONArray filtered = new JSONArray();
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONObject obj = arr.getJSONObject(i);
|
||||||
|
if (!obj.getString("url").equals(url)) {
|
||||||
|
filtered.put(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to front
|
||||||
|
JSONObject entry = new JSONObject();
|
||||||
|
entry.put("url", url);
|
||||||
|
entry.put("title", title != null ? title : url);
|
||||||
|
entry.put("time", System.currentTimeMillis());
|
||||||
|
|
||||||
|
JSONArray newArr = new JSONArray();
|
||||||
|
newArr.put(entry);
|
||||||
|
for (int i = 0; i < Math.min(filtered.length(), 99); i++) {
|
||||||
|
newArr.put(filtered.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit().putString(KEY_HISTORY, newArr.toString()).apply();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String[]> getHistory() {
|
||||||
|
SharedPreferences prefs = getSharedPreferences(PREFS_BROWSER, MODE_PRIVATE);
|
||||||
|
String json = prefs.getString(KEY_HISTORY, "[]");
|
||||||
|
List<String[]> list = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
JSONArray arr = new JSONArray(json);
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONObject obj = arr.getJSONObject(i);
|
||||||
|
list.add(new String[]{obj.getString("title"), obj.getString("url")});
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Browser Menu
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private void showBrowserMenu() {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
|
||||||
|
AeThexContextMenu.MenuItem[] items = new AeThexContextMenu.MenuItem[] {
|
||||||
|
new AeThexContextMenu.MenuItem("home", "Home", "⌂"),
|
||||||
|
new AeThexContextMenu.MenuItem("newtab", "New Tab", "+"),
|
||||||
|
new AeThexContextMenu.MenuItem("bookmarks", "Bookmarks", "★"),
|
||||||
|
new AeThexContextMenu.MenuItem("history", "History", "↺"),
|
||||||
|
new AeThexContextMenu.MenuItem("find", "Find in Page", "⌕"),
|
||||||
|
new AeThexContextMenu.MenuItem("clear", "Clear Data", "⌧"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Position menu at top-right of screen
|
||||||
|
float x = getResources().getDisplayMetrics().widthPixels - dpToPx(16);
|
||||||
|
float y = dpToPx(90);
|
||||||
|
|
||||||
|
AeThexContextMenu.show(this, x, y, "BROWSER", items, actionId -> {
|
||||||
|
switch (actionId) {
|
||||||
|
case "home":
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) loadHomePage(tab);
|
||||||
|
break;
|
||||||
|
case "newtab":
|
||||||
|
addNewTab(null);
|
||||||
|
break;
|
||||||
|
case "bookmarks":
|
||||||
|
showBookmarksPage();
|
||||||
|
break;
|
||||||
|
case "history":
|
||||||
|
showHistoryPage();
|
||||||
|
break;
|
||||||
|
case "find":
|
||||||
|
showFindBar();
|
||||||
|
break;
|
||||||
|
case "clear":
|
||||||
|
clearBrowsingData();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showBookmarksPage() {
|
||||||
|
List<String[]> bookmarks = getBookmarks();
|
||||||
|
int primaryInt = themeManager.getPrimaryColor(this);
|
||||||
|
String ph = String.format("#%06X", (0xFFFFFF & primaryInt));
|
||||||
|
|
||||||
|
StringBuilder rows = new StringBuilder();
|
||||||
|
if (bookmarks.isEmpty()) {
|
||||||
|
rows.append("<div style='text-align:center;color:rgba(255,255,255,0.3);padding:40px;font-size:13px'>No bookmarks yet</div>");
|
||||||
|
} else {
|
||||||
|
for (String[] bm : bookmarks) {
|
||||||
|
rows.append("<a class='item' href='").append(escapeHtml(bm[1])).append("'>")
|
||||||
|
.append("<div class='title'>").append(escapeHtml(bm[0])).append("</div>")
|
||||||
|
.append("<div class='url'>").append(escapeHtml(bm[1])).append("</div>")
|
||||||
|
.append("</a>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String html = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width,initial-scale=1.0'>"
|
||||||
|
+ "<style>*{margin:0;padding:0;box-sizing:border-box}"
|
||||||
|
+ "body{background:#0A0E1A;color:#fff;font-family:'Courier New',monospace;padding:24px 16px}"
|
||||||
|
+ "h1{font-size:12px;color:rgba(255,255,255,0.4);letter-spacing:2px;margin-bottom:16px}"
|
||||||
|
+ ".item{display:block;padding:14px;border-radius:10px;background:rgba(255,255,255,0.03);"
|
||||||
|
+ "border:1px solid rgba(255,255,255,0.05);margin-bottom:8px;text-decoration:none;"
|
||||||
|
+ "transition:all 0.15s}"
|
||||||
|
+ ".item:active{background:" + ph + "15;border-color:" + ph + "30}"
|
||||||
|
+ ".title{font-size:13px;color:rgba(255,255,255,0.8);margin-bottom:4px}"
|
||||||
|
+ ".url{font-size:10px;color:" + ph + ";overflow:hidden;text-overflow:ellipsis;white-space:nowrap}"
|
||||||
|
+ "</style></head><body><h1>★ BOOKMARKS</h1>" + rows.toString() + "</body></html>";
|
||||||
|
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) {
|
||||||
|
tab.title = "Bookmarks";
|
||||||
|
tab.isHome = true;
|
||||||
|
tab.webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
|
||||||
|
urlBar.setText("");
|
||||||
|
urlBar.setHint("Bookmarks");
|
||||||
|
rebuildTabStrip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHistoryPage() {
|
||||||
|
List<String[]> history = getHistory();
|
||||||
|
int primaryInt = themeManager.getPrimaryColor(this);
|
||||||
|
String ph = String.format("#%06X", (0xFFFFFF & primaryInt));
|
||||||
|
|
||||||
|
StringBuilder rows = new StringBuilder();
|
||||||
|
if (history.isEmpty()) {
|
||||||
|
rows.append("<div style='text-align:center;color:rgba(255,255,255,0.3);padding:40px;font-size:13px'>No history yet</div>");
|
||||||
|
} else {
|
||||||
|
int max = Math.min(history.size(), 50);
|
||||||
|
for (int i = 0; i < max; i++) {
|
||||||
|
String[] h = history.get(i);
|
||||||
|
rows.append("<a class='item' href='").append(escapeHtml(h[1])).append("'>")
|
||||||
|
.append("<div class='title'>").append(escapeHtml(h[0])).append("</div>")
|
||||||
|
.append("<div class='url'>").append(escapeHtml(h[1])).append("</div>")
|
||||||
|
.append("</a>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String html = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width,initial-scale=1.0'>"
|
||||||
|
+ "<style>*{margin:0;padding:0;box-sizing:border-box}"
|
||||||
|
+ "body{background:#0A0E1A;color:#fff;font-family:'Courier New',monospace;padding:24px 16px}"
|
||||||
|
+ "h1{font-size:12px;color:rgba(255,255,255,0.4);letter-spacing:2px;margin-bottom:16px}"
|
||||||
|
+ ".item{display:block;padding:14px;border-radius:10px;background:rgba(255,255,255,0.03);"
|
||||||
|
+ "border:1px solid rgba(255,255,255,0.05);margin-bottom:8px;text-decoration:none;transition:all 0.15s}"
|
||||||
|
+ ".item:active{background:" + ph + "15;border-color:" + ph + "30}"
|
||||||
|
+ ".title{font-size:13px;color:rgba(255,255,255,0.8);margin-bottom:4px}"
|
||||||
|
+ ".url{font-size:10px;color:" + ph + ";overflow:hidden;text-overflow:ellipsis;white-space:nowrap}"
|
||||||
|
+ "</style></head><body><h1>↺ HISTORY</h1>" + rows.toString() + "</body></html>";
|
||||||
|
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) {
|
||||||
|
tab.title = "History";
|
||||||
|
tab.isHome = true;
|
||||||
|
tab.webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
|
||||||
|
urlBar.setText("");
|
||||||
|
urlBar.setHint("History");
|
||||||
|
rebuildTabStrip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearBrowsingData() {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) {
|
||||||
|
tab.webView.clearCache(true);
|
||||||
|
tab.webView.clearHistory();
|
||||||
|
}
|
||||||
|
getSharedPreferences(PREFS_BROWSER, MODE_PRIVATE)
|
||||||
|
.edit().remove(KEY_HISTORY).apply();
|
||||||
|
AeThexToast.show(this, "Browsing data cleared", AeThexToast.Type.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Find in Page
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupFindInPage() {
|
||||||
|
findInput.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) {
|
||||||
|
tab.webView.findAllAsync(s.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
findInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) tab.webView.findNext(true);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
findNext.setOnClickListener(v -> {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) tab.webView.findNext(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
findPrev.setOnClickListener(v -> {
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) tab.webView.findNext(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
findClose.setOnClickListener(v -> hideFindBar());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showFindBar() {
|
||||||
|
findBar.setVisibility(View.VISIBLE);
|
||||||
|
findInput.setText("");
|
||||||
|
findInput.requestFocus();
|
||||||
|
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
|
||||||
|
if (imm != null) imm.showSoftInput(findInput, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideFindBar() {
|
||||||
|
findBar.setVisibility(View.GONE);
|
||||||
|
findInput.setText("");
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null) tab.webView.clearMatches();
|
||||||
|
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
|
||||||
|
if (imm != null) imm.hideSoftInputFromWindow(findInput.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Lifecycle
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (findBar.getVisibility() == View.VISIBLE) {
|
||||||
|
hideFindBar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BrowserTab tab = activeTab();
|
||||||
|
if (tab != null && tab.webView.canGoBack()) {
|
||||||
|
tab.webView.goBack();
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
for (BrowserTab tab : tabs) {
|
||||||
|
if (tab.webView != null) {
|
||||||
|
tab.webView.stopLoading();
|
||||||
|
tab.webView.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabs.clear();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
}
|
||||||
187
android/app/src/main/java/com/aethex/os/CalculatorActivity.java
Normal file
187
android/app/src/main/java/com/aethex/os/CalculatorActivity.java
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
public class CalculatorActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private TextView display;
|
||||||
|
private TextView expression;
|
||||||
|
private StringBuilder currentInput = new StringBuilder();
|
||||||
|
private String operator = "";
|
||||||
|
private double operand1 = 0;
|
||||||
|
private boolean newNumber = true;
|
||||||
|
private String fullExpression = "";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_calculator);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
display = findViewById(R.id.calc_display);
|
||||||
|
expression = findViewById(R.id.calc_expression);
|
||||||
|
|
||||||
|
findViewById(R.id.calc_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Number buttons
|
||||||
|
int[] numIds = {R.id.btn_0, R.id.btn_1, R.id.btn_2, R.id.btn_3,
|
||||||
|
R.id.btn_4, R.id.btn_5, R.id.btn_6, R.id.btn_7,
|
||||||
|
R.id.btn_8, R.id.btn_9};
|
||||||
|
for (int i = 0; i < numIds.length; i++) {
|
||||||
|
final int num = i;
|
||||||
|
findViewById(numIds[i]).setOnClickListener(v -> appendDigit(String.valueOf(num)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decimal
|
||||||
|
findViewById(R.id.btn_decimal).setOnClickListener(v -> {
|
||||||
|
if (newNumber) {
|
||||||
|
currentInput = new StringBuilder("0.");
|
||||||
|
newNumber = false;
|
||||||
|
} else if (!currentInput.toString().contains(".")) {
|
||||||
|
currentInput.append(".");
|
||||||
|
}
|
||||||
|
display.setText(currentInput.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
findViewById(R.id.btn_add).setOnClickListener(v -> setOperator("+"));
|
||||||
|
findViewById(R.id.btn_subtract).setOnClickListener(v -> setOperator("-"));
|
||||||
|
findViewById(R.id.btn_multiply).setOnClickListener(v -> setOperator("×"));
|
||||||
|
findViewById(R.id.btn_divide).setOnClickListener(v -> setOperator("÷"));
|
||||||
|
|
||||||
|
// Equals
|
||||||
|
findViewById(R.id.btn_equals).setOnClickListener(v -> calculate());
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
findViewById(R.id.btn_clear).setOnClickListener(v -> {
|
||||||
|
currentInput = new StringBuilder();
|
||||||
|
operator = "";
|
||||||
|
operand1 = 0;
|
||||||
|
newNumber = true;
|
||||||
|
fullExpression = "";
|
||||||
|
display.setText("0");
|
||||||
|
expression.setText("");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Negate
|
||||||
|
findViewById(R.id.btn_negate).setOnClickListener(v -> {
|
||||||
|
if (currentInput.length() > 0) {
|
||||||
|
double val = Double.parseDouble(currentInput.toString());
|
||||||
|
val = -val;
|
||||||
|
currentInput = new StringBuilder(formatNumber(val));
|
||||||
|
display.setText(currentInput.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parentheses (append to expression)
|
||||||
|
findViewById(R.id.btn_paren_open).setOnClickListener(v -> appendDigit("("));
|
||||||
|
findViewById(R.id.btn_paren_close).setOnClickListener(v -> appendDigit(")"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendDigit(String digit) {
|
||||||
|
if (newNumber) {
|
||||||
|
currentInput = new StringBuilder(digit);
|
||||||
|
newNumber = false;
|
||||||
|
} else {
|
||||||
|
currentInput.append(digit);
|
||||||
|
}
|
||||||
|
display.setText(currentInput.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setOperator(String op) {
|
||||||
|
if (currentInput.length() > 0) {
|
||||||
|
if (!operator.isEmpty()) {
|
||||||
|
calculate();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
operand1 = Double.parseDouble(currentInput.toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fullExpression = formatNumber(operand1) + " " + op;
|
||||||
|
expression.setText(fullExpression);
|
||||||
|
operator = op;
|
||||||
|
newNumber = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void calculate() {
|
||||||
|
if (operator.isEmpty() || currentInput.length() == 0) return;
|
||||||
|
|
||||||
|
double operand2;
|
||||||
|
try {
|
||||||
|
operand2 = Double.parseDouble(currentInput.toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double result = 0;
|
||||||
|
switch (operator) {
|
||||||
|
case "+": result = operand1 + operand2; break;
|
||||||
|
case "-": result = operand1 - operand2; break;
|
||||||
|
case "×": result = operand1 * operand2; break;
|
||||||
|
case "÷":
|
||||||
|
if (operand2 == 0) {
|
||||||
|
display.setText("Error");
|
||||||
|
expression.setText("");
|
||||||
|
currentInput = new StringBuilder();
|
||||||
|
operator = "";
|
||||||
|
newNumber = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result = operand1 / operand2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullExpression = formatNumber(operand1) + " " + operator + " " + formatNumber(operand2) + " =";
|
||||||
|
expression.setText(fullExpression);
|
||||||
|
currentInput = new StringBuilder(formatNumber(result));
|
||||||
|
display.setText(currentInput.toString());
|
||||||
|
operator = "";
|
||||||
|
operand1 = result;
|
||||||
|
newNumber = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatNumber(double num) {
|
||||||
|
if (num == (long) num) {
|
||||||
|
return String.valueOf((long) num);
|
||||||
|
}
|
||||||
|
return String.valueOf(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
390
android/app/src/main/java/com/aethex/os/CameraActivity.java
Normal file
390
android/app/src/main/java/com/aethex/os/CameraActivity.java
Normal file
|
|
@ -0,0 +1,390 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.LinearGradient;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class CameraActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private LinearLayout galleryGrid;
|
||||||
|
private FrameLayout viewfinderFrame;
|
||||||
|
private boolean inViewfinder = true;
|
||||||
|
private Random random = new Random();
|
||||||
|
private Typeface monoFont;
|
||||||
|
private Typeface displayFont;
|
||||||
|
private int photoCount = 0;
|
||||||
|
|
||||||
|
// Simulated gallery of gradient "photos"
|
||||||
|
private static final int[][] GALLERY_GRADIENTS = {
|
||||||
|
{0xFF1a1a2e, 0xFF16213e, 0xFF0f3460},
|
||||||
|
{0xFF2d132c, 0xFF801336, 0xFFc72c41},
|
||||||
|
{0xFF0d7377, 0xFF14a3c7, 0xFF32e0c4},
|
||||||
|
{0xFF1b0a2e, 0xFF4a1942, 0xFF8b2fc9},
|
||||||
|
{0xFF0a1628, 0xFF0d3b66, 0xFF23689b},
|
||||||
|
{0xFF1a0505, 0xFF6b0f1a, 0xFFb91c1c},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
monoFont = themeManager.getMonoFont(this);
|
||||||
|
displayFont = themeManager.getDisplayFont(this);
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
TextView title = findViewById(R.id.app_title);
|
||||||
|
title.setText("Camera");
|
||||||
|
TextView nameDisplay = findViewById(R.id.app_name_display);
|
||||||
|
|
||||||
|
findViewById(R.id.app_back).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout content = (LinearLayout) nameDisplay.getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
content.setGravity(Gravity.TOP);
|
||||||
|
content.setPadding(0, 0, 0, 0);
|
||||||
|
buildCameraUI(content);
|
||||||
|
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildCameraUI(LinearLayout parent) {
|
||||||
|
// Viewfinder area (simulated camera preview)
|
||||||
|
viewfinderFrame = new FrameLayout(this);
|
||||||
|
LinearLayout.LayoutParams vfP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f);
|
||||||
|
viewfinderFrame.setLayoutParams(vfP);
|
||||||
|
|
||||||
|
// Dark gradient simulating live preview
|
||||||
|
GradientDrawable previewBg = new GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.TL_BR,
|
||||||
|
new int[]{Color.parseColor("#0a0a1a"), Color.parseColor("#111827"), Color.parseColor("#0a0a1a")});
|
||||||
|
viewfinderFrame.setBackground(previewBg);
|
||||||
|
|
||||||
|
// Viewfinder crosshair overlay
|
||||||
|
LinearLayout crosshair = new LinearLayout(this);
|
||||||
|
crosshair.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
crosshair.setGravity(Gravity.CENTER);
|
||||||
|
FrameLayout.LayoutParams chP = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
crosshair.setLayoutParams(chP);
|
||||||
|
|
||||||
|
// Center focus brackets
|
||||||
|
TextView focusBrackets = new TextView(this);
|
||||||
|
focusBrackets.setText("┌─────────────┐\n│ │\n│ │\n│ │\n└─────────────┘");
|
||||||
|
focusBrackets.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
focusBrackets.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
focusBrackets.setTypeface(monoFont);
|
||||||
|
focusBrackets.setGravity(Gravity.CENTER);
|
||||||
|
crosshair.addView(focusBrackets);
|
||||||
|
|
||||||
|
// Camera info overlay
|
||||||
|
TextView cameraInfo = new TextView(this);
|
||||||
|
cameraInfo.setText("AeThex Camera • AUTO");
|
||||||
|
cameraInfo.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
cameraInfo.setTextColor(Color.parseColor("#33FFFFFF"));
|
||||||
|
cameraInfo.setTypeface(monoFont);
|
||||||
|
cameraInfo.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams ciP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
ciP.topMargin = dpToPx(12);
|
||||||
|
cameraInfo.setLayoutParams(ciP);
|
||||||
|
crosshair.addView(cameraInfo);
|
||||||
|
|
||||||
|
viewfinderFrame.addView(crosshair);
|
||||||
|
|
||||||
|
// Top-left info
|
||||||
|
TextView topInfo = new TextView(this);
|
||||||
|
topInfo.setText("f/1.8 1/60s ISO 400");
|
||||||
|
topInfo.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
topInfo.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
topInfo.setTypeface(monoFont);
|
||||||
|
topInfo.setPadding(dpToPx(16), dpToPx(12), 0, 0);
|
||||||
|
FrameLayout.LayoutParams tiP = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
tiP.gravity = Gravity.TOP | Gravity.START;
|
||||||
|
topInfo.setLayoutParams(tiP);
|
||||||
|
viewfinderFrame.addView(topInfo);
|
||||||
|
|
||||||
|
parent.addView(viewfinderFrame);
|
||||||
|
|
||||||
|
// Controls bar
|
||||||
|
LinearLayout controls = new LinearLayout(this);
|
||||||
|
controls.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
controls.setGravity(Gravity.CENTER);
|
||||||
|
controls.setPadding(dpToPx(20), dpToPx(16), dpToPx(20), dpToPx(16));
|
||||||
|
controls.setBackgroundColor(Color.parseColor("#E6050510"));
|
||||||
|
|
||||||
|
// Gallery button (left)
|
||||||
|
TextView galleryBtn = new TextView(this);
|
||||||
|
galleryBtn.setText("Gallery");
|
||||||
|
galleryBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
galleryBtn.setTextColor(Color.parseColor("#99FFFFFF"));
|
||||||
|
galleryBtn.setTypeface(monoFont);
|
||||||
|
galleryBtn.setPadding(dpToPx(14), dpToPx(8), dpToPx(14), dpToPx(8));
|
||||||
|
GradientDrawable galBg = new GradientDrawable();
|
||||||
|
galBg.setCornerRadius(dpToPx(8));
|
||||||
|
galBg.setColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
galleryBtn.setBackground(galBg);
|
||||||
|
galleryBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
showGallery();
|
||||||
|
});
|
||||||
|
controls.addView(galleryBtn);
|
||||||
|
|
||||||
|
View spacer = new View(this);
|
||||||
|
spacer.setLayoutParams(new LinearLayout.LayoutParams(0, 1, 1f));
|
||||||
|
controls.addView(spacer);
|
||||||
|
|
||||||
|
// Shutter button (center)
|
||||||
|
FrameLayout shutterOuter = new FrameLayout(this);
|
||||||
|
int outerSize = dpToPx(64);
|
||||||
|
shutterOuter.setLayoutParams(new LinearLayout.LayoutParams(outerSize, outerSize));
|
||||||
|
GradientDrawable outerRing = new GradientDrawable();
|
||||||
|
outerRing.setShape(GradientDrawable.OVAL);
|
||||||
|
outerRing.setStroke(dpToPx(3), Color.WHITE);
|
||||||
|
outerRing.setColor(Color.TRANSPARENT);
|
||||||
|
shutterOuter.setBackground(outerRing);
|
||||||
|
|
||||||
|
View shutterInner = new View(this);
|
||||||
|
int innerSize = dpToPx(52);
|
||||||
|
FrameLayout.LayoutParams innerP = new FrameLayout.LayoutParams(innerSize, innerSize);
|
||||||
|
innerP.gravity = Gravity.CENTER;
|
||||||
|
shutterInner.setLayoutParams(innerP);
|
||||||
|
GradientDrawable innerBg = new GradientDrawable();
|
||||||
|
innerBg.setShape(GradientDrawable.OVAL);
|
||||||
|
innerBg.setColor(Color.WHITE);
|
||||||
|
shutterInner.setBackground(innerBg);
|
||||||
|
shutterOuter.addView(shutterInner);
|
||||||
|
|
||||||
|
shutterOuter.setOnClickListener(v -> takePhoto());
|
||||||
|
controls.addView(shutterOuter);
|
||||||
|
|
||||||
|
View spacer2 = new View(this);
|
||||||
|
spacer2.setLayoutParams(new LinearLayout.LayoutParams(0, 1, 1f));
|
||||||
|
controls.addView(spacer2);
|
||||||
|
|
||||||
|
// Switch camera button (right)
|
||||||
|
TextView switchBtn = new TextView(this);
|
||||||
|
switchBtn.setText("⟳ Flip");
|
||||||
|
switchBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
switchBtn.setTextColor(Color.parseColor("#99FFFFFF"));
|
||||||
|
switchBtn.setTypeface(monoFont);
|
||||||
|
switchBtn.setPadding(dpToPx(14), dpToPx(8), dpToPx(14), dpToPx(8));
|
||||||
|
GradientDrawable swBg = new GradientDrawable();
|
||||||
|
swBg.setCornerRadius(dpToPx(8));
|
||||||
|
swBg.setColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
switchBtn.setBackground(swBg);
|
||||||
|
switchBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
AeThexToast.show(this, "Camera flipped", AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
controls.addView(switchBtn);
|
||||||
|
|
||||||
|
parent.addView(controls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void takePhoto() {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
photoCount++;
|
||||||
|
|
||||||
|
// Flash effect
|
||||||
|
View flash = new View(this);
|
||||||
|
flash.setBackgroundColor(Color.WHITE);
|
||||||
|
flash.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
viewfinderFrame.addView(flash);
|
||||||
|
flash.setAlpha(0.8f);
|
||||||
|
flash.animate().alpha(0f).setDuration(300).withEndAction(() -> {
|
||||||
|
viewfinderFrame.removeView(flash);
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
AeThexToast.show(this, "Photo captured (" + photoCount + ")", AeThexToast.Type.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showGallery() {
|
||||||
|
FrameLayout root = (FrameLayout) findViewById(R.id.app_root);
|
||||||
|
|
||||||
|
FrameLayout overlay = new FrameLayout(this);
|
||||||
|
overlay.setTag("gallery_overlay");
|
||||||
|
overlay.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
overlay.setBackgroundColor(Color.parseColor("#F2080810"));
|
||||||
|
|
||||||
|
LinearLayout panel = new LinearLayout(this);
|
||||||
|
panel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
panel.setPadding(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(16));
|
||||||
|
FrameLayout.LayoutParams pp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
panel.setLayoutParams(pp);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
LinearLayout hdr = new LinearLayout(this);
|
||||||
|
hdr.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
TextView backBtn = new TextView(this);
|
||||||
|
backBtn.setText("← Camera");
|
||||||
|
backBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
backBtn.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
backBtn.setTypeface(monoFont);
|
||||||
|
backBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
root.removeView(overlay);
|
||||||
|
});
|
||||||
|
hdr.addView(backBtn);
|
||||||
|
|
||||||
|
View sp = new View(this);
|
||||||
|
sp.setLayoutParams(new LinearLayout.LayoutParams(0, 1, 1f));
|
||||||
|
hdr.addView(sp);
|
||||||
|
|
||||||
|
TextView countTv = new TextView(this);
|
||||||
|
countTv.setText((GALLERY_GRADIENTS.length + photoCount) + " photos");
|
||||||
|
countTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
countTv.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
countTv.setTypeface(monoFont);
|
||||||
|
hdr.addView(countTv);
|
||||||
|
|
||||||
|
panel.addView(hdr);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
TextView label = new TextView(this);
|
||||||
|
label.setText("GALLERY");
|
||||||
|
label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
label.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
label.setTypeface(monoFont);
|
||||||
|
label.setLetterSpacing(0.15f);
|
||||||
|
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
lp.topMargin = dpToPx(16);
|
||||||
|
lp.bottomMargin = dpToPx(12);
|
||||||
|
label.setLayoutParams(lp);
|
||||||
|
panel.addView(label);
|
||||||
|
|
||||||
|
// Photo grid (2 columns)
|
||||||
|
ScrollView scroll = new ScrollView(this);
|
||||||
|
scroll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
|
||||||
|
LinearLayout grid = new LinearLayout(this);
|
||||||
|
grid.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
int totalPhotos = GALLERY_GRADIENTS.length + photoCount;
|
||||||
|
for (int i = 0; i < totalPhotos; i += 2) {
|
||||||
|
LinearLayout row = new LinearLayout(this);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
LinearLayout.LayoutParams rowP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rowP.bottomMargin = dpToPx(6);
|
||||||
|
row.setLayoutParams(rowP);
|
||||||
|
|
||||||
|
for (int j = 0; j < 2 && (i + j) < totalPhotos; j++) {
|
||||||
|
FrameLayout cell = new FrameLayout(this);
|
||||||
|
LinearLayout.LayoutParams cellP = new LinearLayout.LayoutParams(0, dpToPx(120), 1f);
|
||||||
|
if (j == 0) cellP.setMarginEnd(dpToPx(3));
|
||||||
|
else cellP.setMarginStart(dpToPx(3));
|
||||||
|
cell.setLayoutParams(cellP);
|
||||||
|
|
||||||
|
int idx = (i + j) % GALLERY_GRADIENTS.length;
|
||||||
|
GradientDrawable grad = new GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.TL_BR,
|
||||||
|
GALLERY_GRADIENTS[idx]);
|
||||||
|
grad.setCornerRadius(dpToPx(8));
|
||||||
|
cell.setBackground(grad);
|
||||||
|
|
||||||
|
// Date label
|
||||||
|
TextView dateLbl = new TextView(this);
|
||||||
|
dateLbl.setText("IMG_" + String.format("%04d", i + j + 1));
|
||||||
|
dateLbl.setTextSize(TypedValue.COMPLEX_UNIT_SP, 8);
|
||||||
|
dateLbl.setTextColor(Color.parseColor("#80FFFFFF"));
|
||||||
|
dateLbl.setTypeface(monoFont);
|
||||||
|
dateLbl.setPadding(dpToPx(6), 0, 0, dpToPx(4));
|
||||||
|
FrameLayout.LayoutParams dlP = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
dlP.gravity = Gravity.BOTTOM | Gravity.START;
|
||||||
|
dateLbl.setLayoutParams(dlP);
|
||||||
|
cell.addView(dateLbl);
|
||||||
|
|
||||||
|
row.addView(cell);
|
||||||
|
}
|
||||||
|
grid.addView(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll.addView(grid);
|
||||||
|
panel.addView(scroll);
|
||||||
|
overlay.addView(panel);
|
||||||
|
|
||||||
|
overlay.setAlpha(0f);
|
||||||
|
root.addView(overlay);
|
||||||
|
overlay.animate().alpha(1f).setDuration(200).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
FrameLayout root = (FrameLayout) findViewById(R.id.app_root);
|
||||||
|
View gallery = root.findViewWithTag("gallery_overlay");
|
||||||
|
if (gallery != null) {
|
||||||
|
root.removeView(gallery);
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
277
android/app/src/main/java/com/aethex/os/ChatActivity.java
Normal file
277
android/app/src/main/java/com/aethex/os/ChatActivity.java
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class ChatActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private ChatAdapter adapter;
|
||||||
|
private EditText inputField;
|
||||||
|
private final List<Message> messages = new ArrayList<>();
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
private static final String[] AI_RESPONSES = {
|
||||||
|
"Processing your request through the AeThex neural core...",
|
||||||
|
"Interesting query. Let me analyze that for you.",
|
||||||
|
"The AeThex system is operating at optimal capacity.",
|
||||||
|
"I've updated my knowledge base with that information.",
|
||||||
|
"Running diagnostics... All systems nominal.",
|
||||||
|
"That's an excellent observation. The data corroborates your findings.",
|
||||||
|
"I've cross-referenced the AeThex database. Here's what I found...",
|
||||||
|
"System resources are currently at 67% utilization.",
|
||||||
|
"Your clearance level grants access to this information.",
|
||||||
|
"Acknowledged. I'll process that through the quantum mesh network."
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_chat);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
findViewById(R.id.chat_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View root = findViewById(R.id.chat_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
// Set up RecyclerView
|
||||||
|
recyclerView = findViewById(R.id.chat_recycler);
|
||||||
|
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||||
|
layoutManager.setStackFromEnd(true);
|
||||||
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
|
|
||||||
|
adapter = new ChatAdapter();
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
// Set up input
|
||||||
|
inputField = findViewById(R.id.chat_input);
|
||||||
|
TextView sendButton = findViewById(R.id.chat_send);
|
||||||
|
|
||||||
|
sendButton.setOnClickListener(v -> sendMessage());
|
||||||
|
|
||||||
|
inputField.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_SEND ||
|
||||||
|
(event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||||
|
&& event.getAction() == KeyEvent.ACTION_DOWN)) {
|
||||||
|
sendMessage();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pre-populated conversation
|
||||||
|
loadInitialConversation();
|
||||||
|
|
||||||
|
// Custom themed keyboard
|
||||||
|
AeThexKeyboard.attach(this);
|
||||||
|
|
||||||
|
// Add bottom navigation bar
|
||||||
|
BottomNavBar.attach(this, (ViewGroup) findViewById(R.id.chat_root), BottomNavBar.TAB_CHAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadInitialConversation() {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||||
|
|
||||||
|
// Create timestamps that look like they happened earlier
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
String time1 = formatTime(now - 300000); // 5 min ago
|
||||||
|
String time2 = formatTime(now - 240000); // 4 min ago
|
||||||
|
String time3 = formatTime(now - 180000); // 3 min ago
|
||||||
|
|
||||||
|
messages.add(new Message(
|
||||||
|
"Welcome to AeThex OS. I'm your system assistant. How can I help you today?",
|
||||||
|
false, time1));
|
||||||
|
messages.add(new Message(
|
||||||
|
"What can you do?",
|
||||||
|
true, time2));
|
||||||
|
messages.add(new Message(
|
||||||
|
"I can help with system navigation, app recommendations, and general queries. I'm always learning and expanding my capabilities within the AeThex ecosystem.",
|
||||||
|
false, time3));
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage() {
|
||||||
|
String text = inputField.getText().toString().trim();
|
||||||
|
if (text.isEmpty()) return;
|
||||||
|
|
||||||
|
// Add user message
|
||||||
|
String timestamp = formatTime(System.currentTimeMillis());
|
||||||
|
messages.add(new Message(text, true, timestamp));
|
||||||
|
adapter.notifyItemInserted(messages.size() - 1);
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
inputField.setText("");
|
||||||
|
|
||||||
|
// AI auto-reply after 1 second delay
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
String reply = AI_RESPONSES[random.nextInt(AI_RESPONSES.length)];
|
||||||
|
String replyTime = formatTime(System.currentTimeMillis());
|
||||||
|
messages.add(new Message(reply, false, replyTime));
|
||||||
|
adapter.notifyItemInserted(messages.size() - 1);
|
||||||
|
scrollToBottom();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTime(long millis) {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||||
|
return sdf.format(new Date(millis));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollToBottom() {
|
||||||
|
recyclerView.post(() -> {
|
||||||
|
if (messages.size() > 0) {
|
||||||
|
recyclerView.smoothScrollToPosition(messages.size() - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Inner classes ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static class Message {
|
||||||
|
final String text;
|
||||||
|
final boolean isUser;
|
||||||
|
final String timestamp;
|
||||||
|
|
||||||
|
Message(String text, boolean isUser, String timestamp) {
|
||||||
|
this.text = text;
|
||||||
|
this.isUser = isUser;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_chat_message, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
Message msg = messages.get(position);
|
||||||
|
holder.bind(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return messages.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
private final LinearLayout bubble;
|
||||||
|
private final TextView textView;
|
||||||
|
private final TextView timeView;
|
||||||
|
|
||||||
|
ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
bubble = itemView.findViewById(R.id.msg_bubble);
|
||||||
|
textView = itemView.findViewById(R.id.msg_text);
|
||||||
|
timeView = itemView.findViewById(R.id.msg_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(Message msg) {
|
||||||
|
textView.setText(msg.text);
|
||||||
|
timeView.setText(msg.timestamp);
|
||||||
|
|
||||||
|
// Create rounded background programmatically
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(16));
|
||||||
|
|
||||||
|
// Set bubble alignment and colors based on sender
|
||||||
|
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) bubble.getLayoutParams();
|
||||||
|
|
||||||
|
if (msg.isUser) {
|
||||||
|
// User message: right-aligned, primary-tinted
|
||||||
|
params.gravity = Gravity.END;
|
||||||
|
int primary = themeManager.getPrimaryColor(ChatActivity.this);
|
||||||
|
bg.setColor((primary & 0x00FFFFFF) | 0x20000000);
|
||||||
|
textView.setTextColor(0xCCFFFFFF); // text_white_80
|
||||||
|
timeView.setTextColor(0x66FFFFFF); // text_white_40
|
||||||
|
timeView.setGravity(Gravity.END);
|
||||||
|
} else {
|
||||||
|
// AI message: left-aligned, white-10%
|
||||||
|
params.gravity = Gravity.START;
|
||||||
|
bg.setColor(0x1AFFFFFF);
|
||||||
|
textView.setTextColor(0xCCFFFFFF); // text_white_80
|
||||||
|
timeView.setTextColor(0x66FFFFFF); // text_white_40
|
||||||
|
timeView.setGravity(Gravity.START);
|
||||||
|
}
|
||||||
|
|
||||||
|
bubble.setLayoutParams(params);
|
||||||
|
bubble.setBackground(bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
float density = getResources().getDisplayMetrics().density;
|
||||||
|
return Math.round(dp * density);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── System UI ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
|
import android.view.animation.LinearInterpolator;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fullscreen overlay animation for clearance switching.
|
||||||
|
* Shows a cinematic 5-step animation: fade-in → spinner → text → progress line → fade-out.
|
||||||
|
*/
|
||||||
|
public class ClearanceSwitchOverlay {
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void onSwitchComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String OVERLAY_TAG = "clearance_switch_overlay";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the clearance switch overlay animation.
|
||||||
|
*
|
||||||
|
* @param activity The current activity
|
||||||
|
* @param targetMode The clearance mode being switched to (ThemeManager.CLEARANCE_CORP or CLEARANCE_FOUNDATION)
|
||||||
|
* @param callback Called when animation completes and theme should be applied
|
||||||
|
*/
|
||||||
|
public static void show(Activity activity, String targetMode, Callback callback) {
|
||||||
|
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
|
||||||
|
if (callback != null) callback.onSwitchComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
// Remove any existing overlay
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
View existing = decorView.findViewWithTag(OVERLAY_TAG);
|
||||||
|
if (existing != null) {
|
||||||
|
decorView.removeView(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the overlay
|
||||||
|
FrameLayout overlay = createOverlay(activity, targetMode);
|
||||||
|
decorView.addView(overlay);
|
||||||
|
|
||||||
|
// Run the 5-step animation
|
||||||
|
runAnimation(activity, overlay, targetMode, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FrameLayout createOverlay(Activity activity, String targetMode) {
|
||||||
|
int bgColor = ThemeManager.getSwitchOverlayBackground(targetMode);
|
||||||
|
int spinnerColor = ThemeManager.getSwitchSpinnerColor(targetMode);
|
||||||
|
String label = ThemeManager.getSwitchLabel(targetMode);
|
||||||
|
|
||||||
|
FrameLayout overlay = new FrameLayout(activity);
|
||||||
|
overlay.setTag(OVERLAY_TAG);
|
||||||
|
overlay.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
overlay.setBackgroundColor(bgColor);
|
||||||
|
overlay.setAlpha(0f);
|
||||||
|
overlay.setClickable(true); // Block touches below
|
||||||
|
|
||||||
|
// Center content container
|
||||||
|
LinearLayout center = new LinearLayout(activity);
|
||||||
|
center.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
center.setGravity(Gravity.CENTER);
|
||||||
|
FrameLayout.LayoutParams centerParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
centerParams.gravity = Gravity.CENTER;
|
||||||
|
center.setLayoutParams(centerParams);
|
||||||
|
|
||||||
|
// Spinner ring (circular border)
|
||||||
|
View spinner = new View(activity);
|
||||||
|
int spinnerSize = dpToPx(activity, 48);
|
||||||
|
LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams(spinnerSize, spinnerSize);
|
||||||
|
spinnerParams.gravity = Gravity.CENTER_HORIZONTAL;
|
||||||
|
spinner.setLayoutParams(spinnerParams);
|
||||||
|
|
||||||
|
GradientDrawable spinnerBg = new GradientDrawable();
|
||||||
|
spinnerBg.setShape(GradientDrawable.OVAL);
|
||||||
|
spinnerBg.setColor(Color.TRANSPARENT);
|
||||||
|
spinnerBg.setStroke(dpToPx(activity, 3), spinnerColor);
|
||||||
|
spinner.setBackground(spinnerBg);
|
||||||
|
spinner.setAlpha(0f);
|
||||||
|
spinner.setTag("switch_spinner");
|
||||||
|
center.addView(spinner);
|
||||||
|
|
||||||
|
// Label text
|
||||||
|
TextView labelView = new TextView(activity);
|
||||||
|
labelView.setText(label);
|
||||||
|
labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
labelView.setTextColor(Color.argb((int) (255 * 0.8f), 255, 255, 255));
|
||||||
|
labelView.setLetterSpacing(0.2f);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Typeface font = ResourcesCompat.getFont(activity, R.font.source_code_pro);
|
||||||
|
if (font != null) labelView.setTypeface(font, Typeface.BOLD);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
labelView.setTypeface(Typeface.MONOSPACE, Typeface.BOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
labelParams.gravity = Gravity.CENTER_HORIZONTAL;
|
||||||
|
labelParams.topMargin = dpToPx(activity, 20);
|
||||||
|
labelView.setLayoutParams(labelParams);
|
||||||
|
labelView.setAlpha(0f);
|
||||||
|
labelView.setTag("switch_label");
|
||||||
|
center.addView(labelView);
|
||||||
|
|
||||||
|
// Sub-label (mode name)
|
||||||
|
TextView subLabel = new TextView(activity);
|
||||||
|
String modeName = ThemeManager.CLEARANCE_CORP.equals(targetMode)
|
||||||
|
? "CORP CLEARANCE" : "FOUNDATION CLEARANCE";
|
||||||
|
subLabel.setText(modeName);
|
||||||
|
subLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
subLabel.setTextColor(spinnerColor);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Typeface font = ResourcesCompat.getFont(activity, R.font.source_code_pro);
|
||||||
|
if (font != null) subLabel.setTypeface(font);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
subLabel.setTypeface(Typeface.MONOSPACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams subParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
subParams.gravity = Gravity.CENTER_HORIZONTAL;
|
||||||
|
subParams.topMargin = dpToPx(activity, 6);
|
||||||
|
subLabel.setLayoutParams(subParams);
|
||||||
|
subLabel.setAlpha(0f);
|
||||||
|
subLabel.setTag("switch_sub_label");
|
||||||
|
center.addView(subLabel);
|
||||||
|
|
||||||
|
overlay.addView(center);
|
||||||
|
|
||||||
|
// Bottom progress line
|
||||||
|
View progressLine = new View(activity);
|
||||||
|
int lineHeight = dpToPx(activity, 2);
|
||||||
|
FrameLayout.LayoutParams lineParams = new FrameLayout.LayoutParams(0, lineHeight);
|
||||||
|
lineParams.gravity = Gravity.BOTTOM;
|
||||||
|
progressLine.setLayoutParams(lineParams);
|
||||||
|
progressLine.setBackgroundColor(spinnerColor);
|
||||||
|
progressLine.setTag("switch_progress");
|
||||||
|
overlay.addView(progressLine);
|
||||||
|
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runAnimation(Activity activity, FrameLayout overlay,
|
||||||
|
String targetMode, Callback callback) {
|
||||||
|
Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
int spinnerColor = ThemeManager.getSwitchSpinnerColor(targetMode);
|
||||||
|
|
||||||
|
View spinner = overlay.findViewWithTag("switch_spinner");
|
||||||
|
View label = overlay.findViewWithTag("switch_label");
|
||||||
|
View subLabel = overlay.findViewWithTag("switch_sub_label");
|
||||||
|
View progress = overlay.findViewWithTag("switch_progress");
|
||||||
|
|
||||||
|
// Play switch sound
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.SWITCH);
|
||||||
|
|
||||||
|
// Step 1: Fade in overlay (200ms)
|
||||||
|
overlay.animate().alpha(1f).setDuration(200).withEndAction(() -> {
|
||||||
|
|
||||||
|
// Step 2: Show spinner + spin (400ms)
|
||||||
|
spinner.setAlpha(1f);
|
||||||
|
spinner.setScaleX(0.5f);
|
||||||
|
spinner.setScaleY(0.5f);
|
||||||
|
spinner.animate().scaleX(1f).scaleY(1f).setDuration(300)
|
||||||
|
.setInterpolator(new AccelerateDecelerateInterpolator()).start();
|
||||||
|
|
||||||
|
ObjectAnimator rotation = ObjectAnimator.ofFloat(spinner, "rotation", 0f, 360f);
|
||||||
|
rotation.setDuration(800);
|
||||||
|
rotation.setRepeatCount(1);
|
||||||
|
rotation.setInterpolator(new LinearInterpolator());
|
||||||
|
rotation.start();
|
||||||
|
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
// Step 3: Show text (300ms)
|
||||||
|
label.animate().alpha(1f).setDuration(200).start();
|
||||||
|
subLabel.animate().alpha(1f).setDuration(200).setStartDelay(100).start();
|
||||||
|
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
// Step 4: Progress line sweeps across (600ms)
|
||||||
|
int screenWidth = activity.getWindow().getDecorView().getWidth();
|
||||||
|
ValueAnimator lineAnim = ValueAnimator.ofInt(0, screenWidth);
|
||||||
|
lineAnim.setDuration(600);
|
||||||
|
lineAnim.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||||
|
lineAnim.addUpdateListener(anim -> {
|
||||||
|
int val = (int) anim.getAnimatedValue();
|
||||||
|
ViewGroup.LayoutParams lp = progress.getLayoutParams();
|
||||||
|
lp.width = val;
|
||||||
|
progress.setLayoutParams(lp);
|
||||||
|
});
|
||||||
|
lineAnim.start();
|
||||||
|
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
// Step 5: Fade out (300ms)
|
||||||
|
overlay.animate().alpha(0f).setDuration(300).withEndAction(() -> {
|
||||||
|
// Remove overlay
|
||||||
|
FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView();
|
||||||
|
decorView.removeView(overlay);
|
||||||
|
|
||||||
|
// Invoke callback
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onSwitchComplete();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}, 650);
|
||||||
|
|
||||||
|
}, 350);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int dpToPx(Activity activity, float dp) {
|
||||||
|
return Math.round(TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, dp,
|
||||||
|
activity.getResources().getDisplayMetrics()));
|
||||||
|
}
|
||||||
|
}
|
||||||
605
android/app/src/main/java/com/aethex/os/ClockActivity.java
Normal file
605
android/app/src/main/java/com/aethex/os/ClockActivity.java
Normal file
|
|
@ -0,0 +1,605 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.media.ToneGenerator;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.CountDownTimer;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.VibrationEffect;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.ViewFlipper;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
public class ClockActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
|
||||||
|
// --- Tab views ---
|
||||||
|
private TextView tabClock, tabStopwatch, tabTimer;
|
||||||
|
private View tabIndicator;
|
||||||
|
private ViewFlipper viewFlipper;
|
||||||
|
|
||||||
|
// --- Clock tab ---
|
||||||
|
private TextView clockTimeDisplay, clockSecondsDisplay, clockAmPm;
|
||||||
|
private TextView clockDateDisplay, clockTimezone;
|
||||||
|
private Handler clockHandler;
|
||||||
|
|
||||||
|
// --- Stopwatch tab ---
|
||||||
|
private TextView stopwatchDisplay;
|
||||||
|
private TextView stopwatchStart, stopwatchReset, stopwatchLap;
|
||||||
|
private LinearLayout lapContainer;
|
||||||
|
private LinearLayout lapHeader;
|
||||||
|
private ScrollView lapScroll;
|
||||||
|
private Handler stopwatchHandler;
|
||||||
|
private long stopwatchStartTime = 0;
|
||||||
|
private long stopwatchElapsed = 0;
|
||||||
|
private boolean stopwatchRunning = false;
|
||||||
|
private List<Long> lapTimes = new ArrayList<>();
|
||||||
|
private long lastLapTime = 0;
|
||||||
|
|
||||||
|
// --- Timer tab ---
|
||||||
|
private TextView timerHours, timerMinutes, timerSeconds;
|
||||||
|
private TextView timerCountdownDisplay, timerProgressLabel;
|
||||||
|
private LinearLayout timerInputArea;
|
||||||
|
private TextView timerStart, timerReset;
|
||||||
|
private int timerH = 0, timerM = 5, timerS = 0;
|
||||||
|
private CountDownTimer countDownTimer;
|
||||||
|
private long timerRemainingMs = 0;
|
||||||
|
private long timerTotalMs = 0;
|
||||||
|
private boolean timerRunning = false;
|
||||||
|
private boolean timerPaused = false;
|
||||||
|
|
||||||
|
private int currentTab = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_clock);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View root = findViewById(R.id.clock_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
findViewById(R.id.clock_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
tabClock = findViewById(R.id.tab_clock);
|
||||||
|
tabStopwatch = findViewById(R.id.tab_stopwatch);
|
||||||
|
tabTimer = findViewById(R.id.tab_timer);
|
||||||
|
tabIndicator = findViewById(R.id.tab_indicator);
|
||||||
|
viewFlipper = findViewById(R.id.view_flipper);
|
||||||
|
|
||||||
|
tabClock.setOnClickListener(v -> switchTab(0));
|
||||||
|
tabStopwatch.setOnClickListener(v -> switchTab(1));
|
||||||
|
tabTimer.setOnClickListener(v -> switchTab(2));
|
||||||
|
|
||||||
|
// Clock tab views
|
||||||
|
clockTimeDisplay = findViewById(R.id.clock_time_display);
|
||||||
|
clockSecondsDisplay = findViewById(R.id.clock_seconds_display);
|
||||||
|
clockAmPm = findViewById(R.id.clock_ampm);
|
||||||
|
clockDateDisplay = findViewById(R.id.clock_date_display);
|
||||||
|
clockTimezone = findViewById(R.id.clock_timezone);
|
||||||
|
|
||||||
|
// Stopwatch tab views
|
||||||
|
stopwatchDisplay = findViewById(R.id.stopwatch_display);
|
||||||
|
stopwatchStart = findViewById(R.id.stopwatch_start);
|
||||||
|
stopwatchReset = findViewById(R.id.stopwatch_reset);
|
||||||
|
stopwatchLap = findViewById(R.id.stopwatch_lap);
|
||||||
|
lapContainer = findViewById(R.id.lap_container);
|
||||||
|
lapHeader = findViewById(R.id.lap_header);
|
||||||
|
lapScroll = findViewById(R.id.lap_scroll);
|
||||||
|
|
||||||
|
// Timer tab views
|
||||||
|
timerHours = findViewById(R.id.timer_hours);
|
||||||
|
timerMinutes = findViewById(R.id.timer_minutes);
|
||||||
|
timerSeconds = findViewById(R.id.timer_seconds);
|
||||||
|
timerCountdownDisplay = findViewById(R.id.timer_countdown_display);
|
||||||
|
timerProgressLabel = findViewById(R.id.timer_progress_label);
|
||||||
|
timerInputArea = findViewById(R.id.timer_input_area);
|
||||||
|
timerStart = findViewById(R.id.timer_start);
|
||||||
|
timerReset = findViewById(R.id.timer_reset);
|
||||||
|
|
||||||
|
// Initialize handlers
|
||||||
|
clockHandler = new Handler(Looper.getMainLooper());
|
||||||
|
stopwatchHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
// Setup clock
|
||||||
|
startClockUpdates();
|
||||||
|
|
||||||
|
// Setup stopwatch
|
||||||
|
setupStopwatch();
|
||||||
|
|
||||||
|
// Setup timer
|
||||||
|
setupTimer();
|
||||||
|
|
||||||
|
// Set initial tab indicator position
|
||||||
|
tabClock.post(() -> updateTabIndicator(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// TAB SWITCHING
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
private void switchTab(int tab) {
|
||||||
|
if (tab == currentTab) return;
|
||||||
|
currentTab = tab;
|
||||||
|
viewFlipper.setDisplayedChild(tab);
|
||||||
|
updateTabStyles(tab);
|
||||||
|
updateTabIndicator(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTabStyles(int activeTab) {
|
||||||
|
int activeColor = themeManager.getPrimaryColor(ClockActivity.this);
|
||||||
|
int inactiveColor = 0x66FFFFFF;
|
||||||
|
|
||||||
|
tabClock.setTextColor(activeTab == 0 ? activeColor : inactiveColor);
|
||||||
|
tabStopwatch.setTextColor(activeTab == 1 ? activeColor : inactiveColor);
|
||||||
|
tabTimer.setTextColor(activeTab == 2 ? activeColor : inactiveColor);
|
||||||
|
|
||||||
|
tabClock.setTextSize(13);
|
||||||
|
tabStopwatch.setTextSize(13);
|
||||||
|
tabTimer.setTextSize(13);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTabIndicator(int tab) {
|
||||||
|
int parentWidth = ((View) tabIndicator.getParent()).getWidth();
|
||||||
|
if (parentWidth == 0) return;
|
||||||
|
|
||||||
|
int tabWidth = parentWidth / 3;
|
||||||
|
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) tabIndicator.getLayoutParams();
|
||||||
|
params.width = tabWidth;
|
||||||
|
params.gravity = Gravity.START;
|
||||||
|
tabIndicator.setLayoutParams(params);
|
||||||
|
|
||||||
|
tabIndicator.animate()
|
||||||
|
.translationX(tab * tabWidth)
|
||||||
|
.setDuration(200)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// CLOCK TAB
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
private void startClockUpdates() {
|
||||||
|
clockHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateClockDisplay();
|
||||||
|
clockHandler.postDelayed(this, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateClockDisplay() {
|
||||||
|
Date now = new Date();
|
||||||
|
|
||||||
|
// Time (HH:mm)
|
||||||
|
SimpleDateFormat timeFmt = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||||
|
clockTimeDisplay.setText(timeFmt.format(now));
|
||||||
|
|
||||||
|
// Seconds
|
||||||
|
SimpleDateFormat secFmt = new SimpleDateFormat(":ss", Locale.getDefault());
|
||||||
|
clockSecondsDisplay.setText(secFmt.format(now));
|
||||||
|
|
||||||
|
// AM/PM (only show if 12-hour format is used by locale)
|
||||||
|
SimpleDateFormat ampmFmt = new SimpleDateFormat("a", Locale.getDefault());
|
||||||
|
String ampm = ampmFmt.format(now);
|
||||||
|
// Check if device uses 24h - if the formatted hour goes above 12 it's 24h
|
||||||
|
SimpleDateFormat hourCheck = new SimpleDateFormat("H", Locale.getDefault());
|
||||||
|
int hour24 = Integer.parseInt(hourCheck.format(now));
|
||||||
|
if (android.text.format.DateFormat.is24HourFormat(this)) {
|
||||||
|
clockAmPm.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
clockAmPm.setVisibility(View.VISIBLE);
|
||||||
|
clockAmPm.setText(ampm);
|
||||||
|
// Reformat to 12h
|
||||||
|
SimpleDateFormat time12Fmt = new SimpleDateFormat("hh:mm", Locale.getDefault());
|
||||||
|
clockTimeDisplay.setText(time12Fmt.format(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
SimpleDateFormat dateFmt = new SimpleDateFormat("EEEE, MMMM d, yyyy", Locale.getDefault());
|
||||||
|
clockDateDisplay.setText(dateFmt.format(now));
|
||||||
|
|
||||||
|
// Timezone
|
||||||
|
TimeZone tz = TimeZone.getDefault();
|
||||||
|
String tzName = tz.getDisplayName(tz.inDaylightTime(now), TimeZone.SHORT, Locale.getDefault());
|
||||||
|
String tzId = tz.getID();
|
||||||
|
clockTimezone.setText(tzName + " (" + tzId + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// STOPWATCH TAB
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
private void setupStopwatch() {
|
||||||
|
stopwatchStart.setOnClickListener(v -> {
|
||||||
|
if (stopwatchRunning) {
|
||||||
|
stopStopwatch();
|
||||||
|
} else {
|
||||||
|
startStopwatch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopwatchReset.setOnClickListener(v -> resetStopwatch());
|
||||||
|
|
||||||
|
stopwatchLap.setOnClickListener(v -> {
|
||||||
|
if (stopwatchRunning) {
|
||||||
|
recordLap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startStopwatch() {
|
||||||
|
stopwatchRunning = true;
|
||||||
|
stopwatchStartTime = System.currentTimeMillis() - stopwatchElapsed;
|
||||||
|
stopwatchStart.setText("Stop");
|
||||||
|
stopwatchStart.setTextColor(0xFFF87171); // red tint for stop
|
||||||
|
|
||||||
|
stopwatchHandler.post(stopwatchRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopStopwatch() {
|
||||||
|
stopwatchRunning = false;
|
||||||
|
stopwatchElapsed = System.currentTimeMillis() - stopwatchStartTime;
|
||||||
|
stopwatchStart.setText("Start");
|
||||||
|
stopwatchStart.setTextColor(0xFFFFFFFF);
|
||||||
|
stopwatchHandler.removeCallbacks(stopwatchRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetStopwatch() {
|
||||||
|
stopwatchRunning = false;
|
||||||
|
stopwatchElapsed = 0;
|
||||||
|
stopwatchStartTime = 0;
|
||||||
|
lastLapTime = 0;
|
||||||
|
lapTimes.clear();
|
||||||
|
stopwatchDisplay.setText("00:00.00");
|
||||||
|
stopwatchStart.setText("Start");
|
||||||
|
stopwatchStart.setTextColor(0xFFFFFFFF);
|
||||||
|
lapContainer.removeAllViews();
|
||||||
|
lapHeader.setVisibility(View.GONE);
|
||||||
|
stopwatchHandler.removeCallbacks(stopwatchRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Runnable stopwatchRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (stopwatchRunning) {
|
||||||
|
long elapsed = System.currentTimeMillis() - stopwatchStartTime;
|
||||||
|
stopwatchDisplay.setText(formatStopwatchTime(elapsed));
|
||||||
|
stopwatchHandler.postDelayed(this, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void recordLap() {
|
||||||
|
long elapsed = System.currentTimeMillis() - stopwatchStartTime;
|
||||||
|
long lapTime = elapsed - lastLapTime;
|
||||||
|
lastLapTime = elapsed;
|
||||||
|
lapTimes.add(elapsed);
|
||||||
|
|
||||||
|
lapHeader.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
int lapNum = lapTimes.size();
|
||||||
|
|
||||||
|
// Create lap row
|
||||||
|
LinearLayout row = new LinearLayout(this);
|
||||||
|
row.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setPadding(dpToPx(12), dpToPx(10), dpToPx(12), dpToPx(10));
|
||||||
|
row.setBackgroundResource(R.drawable.bg_card_surface);
|
||||||
|
LinearLayout.LayoutParams rowMargin = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rowMargin.setMargins(0, dpToPx(4), 0, 0);
|
||||||
|
row.setLayoutParams(rowMargin);
|
||||||
|
|
||||||
|
// Lap number
|
||||||
|
TextView lapNumTv = new TextView(this);
|
||||||
|
lapNumTv.setLayoutParams(new LinearLayout.LayoutParams(0,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
lapNumTv.setText(String.format(Locale.getDefault(), "#%d", lapNum));
|
||||||
|
lapNumTv.setTextSize(13);
|
||||||
|
lapNumTv.setTextColor(themeManager.getPrimaryColor(ClockActivity.this));
|
||||||
|
lapNumTv.setTypeface(themeManager.getMonoFont(ClockActivity.this));
|
||||||
|
|
||||||
|
// Lap split time
|
||||||
|
TextView lapTimeTv = new TextView(this);
|
||||||
|
lapTimeTv.setLayoutParams(new LinearLayout.LayoutParams(0,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
lapTimeTv.setText(formatStopwatchTime(lapTime));
|
||||||
|
lapTimeTv.setTextSize(13);
|
||||||
|
lapTimeTv.setTextColor(0xCCFFFFFF);
|
||||||
|
lapTimeTv.setTypeface(themeManager.getMonoFont(ClockActivity.this));
|
||||||
|
lapTimeTv.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
// Total time
|
||||||
|
TextView totalTv = new TextView(this);
|
||||||
|
totalTv.setLayoutParams(new LinearLayout.LayoutParams(0,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
totalTv.setText(formatStopwatchTime(elapsed));
|
||||||
|
totalTv.setTextSize(13);
|
||||||
|
totalTv.setTextColor(0x66FFFFFF);
|
||||||
|
totalTv.setTypeface(themeManager.getMonoFont(ClockActivity.this));
|
||||||
|
totalTv.setGravity(Gravity.END);
|
||||||
|
|
||||||
|
row.addView(lapNumTv);
|
||||||
|
row.addView(lapTimeTv);
|
||||||
|
row.addView(totalTv);
|
||||||
|
|
||||||
|
// Add at top (most recent first)
|
||||||
|
lapContainer.addView(row, 0);
|
||||||
|
|
||||||
|
// Scroll to top to show new lap
|
||||||
|
lapScroll.post(() -> lapScroll.fullScroll(View.FOCUS_UP));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatStopwatchTime(long ms) {
|
||||||
|
long totalSecs = ms / 1000;
|
||||||
|
long mins = totalSecs / 60;
|
||||||
|
long secs = totalSecs % 60;
|
||||||
|
long centis = (ms % 1000) / 10;
|
||||||
|
return String.format(Locale.getDefault(), "%02d:%02d.%02d", mins, secs, centis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// TIMER TAB
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
private void setupTimer() {
|
||||||
|
// Up/Down buttons for hours
|
||||||
|
findViewById(R.id.timer_hour_up).setOnClickListener(v -> {
|
||||||
|
if (!timerRunning) {
|
||||||
|
timerH = (timerH + 1) % 24;
|
||||||
|
timerHours.setText(String.format(Locale.getDefault(), "%02d", timerH));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
findViewById(R.id.timer_hour_down).setOnClickListener(v -> {
|
||||||
|
if (!timerRunning) {
|
||||||
|
timerH = (timerH - 1 + 24) % 24;
|
||||||
|
timerHours.setText(String.format(Locale.getDefault(), "%02d", timerH));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Up/Down buttons for minutes
|
||||||
|
findViewById(R.id.timer_min_up).setOnClickListener(v -> {
|
||||||
|
if (!timerRunning) {
|
||||||
|
timerM = (timerM + 1) % 60;
|
||||||
|
timerMinutes.setText(String.format(Locale.getDefault(), "%02d", timerM));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
findViewById(R.id.timer_min_down).setOnClickListener(v -> {
|
||||||
|
if (!timerRunning) {
|
||||||
|
timerM = (timerM - 1 + 60) % 60;
|
||||||
|
timerMinutes.setText(String.format(Locale.getDefault(), "%02d", timerM));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Up/Down buttons for seconds
|
||||||
|
findViewById(R.id.timer_sec_up).setOnClickListener(v -> {
|
||||||
|
if (!timerRunning) {
|
||||||
|
timerS = (timerS + 1) % 60;
|
||||||
|
timerSeconds.setText(String.format(Locale.getDefault(), "%02d", timerS));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
findViewById(R.id.timer_sec_down).setOnClickListener(v -> {
|
||||||
|
if (!timerRunning) {
|
||||||
|
timerS = (timerS - 1 + 60) % 60;
|
||||||
|
timerSeconds.setText(String.format(Locale.getDefault(), "%02d", timerS));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start/Pause button
|
||||||
|
timerStart.setOnClickListener(v -> {
|
||||||
|
if (timerRunning) {
|
||||||
|
pauseTimer();
|
||||||
|
} else {
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
timerReset.setOnClickListener(v -> resetTimer());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startTimer() {
|
||||||
|
long totalMs;
|
||||||
|
if (timerPaused && timerRemainingMs > 0) {
|
||||||
|
totalMs = timerRemainingMs;
|
||||||
|
} else {
|
||||||
|
totalMs = (timerH * 3600L + timerM * 60L + timerS) * 1000L;
|
||||||
|
timerTotalMs = totalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalMs <= 0) return;
|
||||||
|
|
||||||
|
timerRunning = true;
|
||||||
|
timerPaused = false;
|
||||||
|
|
||||||
|
// Switch to countdown display
|
||||||
|
timerInputArea.setVisibility(View.GONE);
|
||||||
|
timerCountdownDisplay.setVisibility(View.VISIBLE);
|
||||||
|
timerProgressLabel.setVisibility(View.VISIBLE);
|
||||||
|
timerStart.setText("Pause");
|
||||||
|
timerStart.setTextColor(0xFFFBBF24); // amber for pause
|
||||||
|
|
||||||
|
countDownTimer = new CountDownTimer(totalMs, 50) {
|
||||||
|
@Override
|
||||||
|
public void onTick(long millisUntilFinished) {
|
||||||
|
timerRemainingMs = millisUntilFinished;
|
||||||
|
updateTimerDisplay(millisUntilFinished);
|
||||||
|
|
||||||
|
// Progress percentage
|
||||||
|
if (timerTotalMs > 0) {
|
||||||
|
int pct = (int) (((timerTotalMs - millisUntilFinished) * 100) / timerTotalMs);
|
||||||
|
timerProgressLabel.setText(pct + "% elapsed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFinish() {
|
||||||
|
timerRemainingMs = 0;
|
||||||
|
timerRunning = false;
|
||||||
|
timerPaused = false;
|
||||||
|
timerCountdownDisplay.setText("00:00:00");
|
||||||
|
timerStart.setText("Start");
|
||||||
|
timerStart.setTextColor(0xFFFFFFFF);
|
||||||
|
timerProgressLabel.setText("Complete!");
|
||||||
|
timerProgressLabel.setTextColor(themeManager.getPrimaryColor(ClockActivity.this));
|
||||||
|
|
||||||
|
// Flash the display
|
||||||
|
timerCountdownDisplay.setTextColor(themeManager.getPrimaryColor(ClockActivity.this));
|
||||||
|
timerCountdownDisplay.animate()
|
||||||
|
.alpha(0.3f).setDuration(300)
|
||||||
|
.withEndAction(() -> timerCountdownDisplay.animate()
|
||||||
|
.alpha(1f).setDuration(300)
|
||||||
|
.withEndAction(() -> timerCountdownDisplay.animate()
|
||||||
|
.alpha(0.3f).setDuration(300)
|
||||||
|
.withEndAction(() -> timerCountdownDisplay.animate()
|
||||||
|
.alpha(1f).setDuration(300)
|
||||||
|
.withEndAction(() -> timerCountdownDisplay.setTextColor(0xFFFFFFFF))
|
||||||
|
.start())
|
||||||
|
.start())
|
||||||
|
.start())
|
||||||
|
.start();
|
||||||
|
|
||||||
|
// Vibrate
|
||||||
|
triggerTimerAlarm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
countDownTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pauseTimer() {
|
||||||
|
if (countDownTimer != null) {
|
||||||
|
countDownTimer.cancel();
|
||||||
|
}
|
||||||
|
timerRunning = false;
|
||||||
|
timerPaused = true;
|
||||||
|
timerStart.setText("Resume");
|
||||||
|
timerStart.setTextColor(0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetTimer() {
|
||||||
|
if (countDownTimer != null) {
|
||||||
|
countDownTimer.cancel();
|
||||||
|
}
|
||||||
|
timerRunning = false;
|
||||||
|
timerPaused = false;
|
||||||
|
timerRemainingMs = 0;
|
||||||
|
|
||||||
|
// Switch back to input display
|
||||||
|
timerInputArea.setVisibility(View.VISIBLE);
|
||||||
|
timerCountdownDisplay.setVisibility(View.GONE);
|
||||||
|
timerProgressLabel.setVisibility(View.GONE);
|
||||||
|
timerStart.setText("Start");
|
||||||
|
timerStart.setTextColor(0xFFFFFFFF);
|
||||||
|
timerProgressLabel.setTextColor(0x66FFFFFF);
|
||||||
|
timerCountdownDisplay.setTextColor(0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTimerDisplay(long ms) {
|
||||||
|
long totalSecs = (ms + 999) / 1000; // Round up to show correct second
|
||||||
|
long h = totalSecs / 3600;
|
||||||
|
long m = (totalSecs % 3600) / 60;
|
||||||
|
long s = totalSecs % 60;
|
||||||
|
timerCountdownDisplay.setText(
|
||||||
|
String.format(Locale.getDefault(), "%02d:%02d:%02d", h, m, s));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerTimerAlarm() {
|
||||||
|
// Vibrate pattern
|
||||||
|
try {
|
||||||
|
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
|
||||||
|
if (vibrator != null && vibrator.hasVibrator()) {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||||
|
long[] pattern = {0, 200, 100, 200, 100, 400};
|
||||||
|
vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1));
|
||||||
|
} else {
|
||||||
|
long[] pattern = {0, 200, 100, 200, 100, 400};
|
||||||
|
vibrator.vibrate(pattern, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// Beep tone
|
||||||
|
try {
|
||||||
|
ToneGenerator toneGen = new ToneGenerator(AudioManager.STREAM_ALARM, 80);
|
||||||
|
toneGen.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 500);
|
||||||
|
// Release after a delay
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(toneGen::release, 1000);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// UTILITY
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// LIFECYCLE
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
clockHandler.removeCallbacksAndMessages(null);
|
||||||
|
stopwatchHandler.removeCallbacks(stopwatchRunnable);
|
||||||
|
if (countDownTimer != null) {
|
||||||
|
countDownTimer.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
354
android/app/src/main/java/com/aethex/os/DataAnalyzerWidget.java
Normal file
354
android/app/src/main/java/com/aethex/os/DataAnalyzerWidget.java
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.BatteryManager;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DataAnalyzerWidget - Real-time system stats widget for desktop.
|
||||||
|
* Shows CPU, RAM, Battery, and Storage usage with animated graphs.
|
||||||
|
* Unlocked by installing "Data Analyzer" from Marketplace.
|
||||||
|
*/
|
||||||
|
public class DataAnalyzerWidget extends LinearLayout {
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private Handler handler;
|
||||||
|
private Runnable updateRunnable;
|
||||||
|
|
||||||
|
// Stats display
|
||||||
|
private TextView cpuValue;
|
||||||
|
private TextView ramValue;
|
||||||
|
private TextView batteryValue;
|
||||||
|
private TextView storageValue;
|
||||||
|
|
||||||
|
// Graph views
|
||||||
|
private GraphView cpuGraph;
|
||||||
|
private GraphView ramGraph;
|
||||||
|
|
||||||
|
// Update interval (ms)
|
||||||
|
private static final int UPDATE_INTERVAL = 2000;
|
||||||
|
|
||||||
|
// Graph history
|
||||||
|
private float[] cpuHistory = new float[20];
|
||||||
|
private float[] ramHistory = new float[20];
|
||||||
|
private int historyIndex = 0;
|
||||||
|
|
||||||
|
public DataAnalyzerWidget(Context context) {
|
||||||
|
super(context);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataAnalyzerWidget(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
this.themeManager = new ThemeManager(context);
|
||||||
|
this.handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
setOrientation(VERTICAL);
|
||||||
|
setPadding(dpToPx(14), dpToPx(12), dpToPx(14), dpToPx(12));
|
||||||
|
|
||||||
|
// Widget background
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(12));
|
||||||
|
bg.setColor(Color.parseColor("#1A0F172A"));
|
||||||
|
int primary = themeManager.getPrimaryColor(context);
|
||||||
|
bg.setStroke(dpToPx(1), Color.argb(40, Color.red(primary), Color.green(primary), Color.blue(primary)));
|
||||||
|
setBackground(bg);
|
||||||
|
|
||||||
|
buildUI();
|
||||||
|
startUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildUI() {
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(context);
|
||||||
|
android.graphics.Typeface monoFont = themeManager.getMonoFont(context);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
TextView header = new TextView(context);
|
||||||
|
header.setText("◉ SYSTEM ANALYZER");
|
||||||
|
header.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
header.setTextColor(primaryColor);
|
||||||
|
header.setTypeface(monoFont, android.graphics.Typeface.BOLD);
|
||||||
|
header.setLetterSpacing(0.15f);
|
||||||
|
addView(header);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
View divider = new View(context);
|
||||||
|
divider.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
LinearLayout.LayoutParams divParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(1));
|
||||||
|
divParams.topMargin = dpToPx(8);
|
||||||
|
divParams.bottomMargin = dpToPx(10);
|
||||||
|
divider.setLayoutParams(divParams);
|
||||||
|
addView(divider);
|
||||||
|
|
||||||
|
// Stats grid (2x2)
|
||||||
|
LinearLayout grid = new LinearLayout(context);
|
||||||
|
grid.setOrientation(VERTICAL);
|
||||||
|
addView(grid);
|
||||||
|
|
||||||
|
// Row 1: CPU & RAM
|
||||||
|
LinearLayout row1 = new LinearLayout(context);
|
||||||
|
row1.setOrientation(HORIZONTAL);
|
||||||
|
row1.setWeightSum(2);
|
||||||
|
grid.addView(row1);
|
||||||
|
|
||||||
|
row1.addView(createStatItem("CPU", "0%", true));
|
||||||
|
row1.addView(createStatItem("RAM", "0%", false));
|
||||||
|
|
||||||
|
// Row 2: Battery & Storage
|
||||||
|
LinearLayout row2 = new LinearLayout(context);
|
||||||
|
row2.setOrientation(HORIZONTAL);
|
||||||
|
row2.setWeightSum(2);
|
||||||
|
LinearLayout.LayoutParams row2Params = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
row2Params.topMargin = dpToPx(8);
|
||||||
|
row2.setLayoutParams(row2Params);
|
||||||
|
grid.addView(row2);
|
||||||
|
|
||||||
|
row2.addView(createStatItem("BATT", "0%", true));
|
||||||
|
row2.addView(createStatItem("DISK", "0%", false));
|
||||||
|
|
||||||
|
// Mini graphs
|
||||||
|
LinearLayout graphContainer = new LinearLayout(context);
|
||||||
|
graphContainer.setOrientation(HORIZONTAL);
|
||||||
|
LinearLayout.LayoutParams graphParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(40));
|
||||||
|
graphParams.topMargin = dpToPx(12);
|
||||||
|
graphContainer.setLayoutParams(graphParams);
|
||||||
|
addView(graphContainer);
|
||||||
|
|
||||||
|
cpuGraph = new GraphView(context, primaryColor, "CPU");
|
||||||
|
ramGraph = new GraphView(context, Color.parseColor("#A855F7"), "RAM");
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams gp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f);
|
||||||
|
gp.setMarginEnd(dpToPx(6));
|
||||||
|
cpuGraph.setLayoutParams(gp);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams gp2 = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f);
|
||||||
|
gp2.setMarginStart(dpToPx(6));
|
||||||
|
ramGraph.setLayoutParams(gp2);
|
||||||
|
|
||||||
|
graphContainer.addView(cpuGraph);
|
||||||
|
graphContainer.addView(ramGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout createStatItem(String label, String value, boolean isLeft) {
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(context);
|
||||||
|
android.graphics.Typeface monoFont = themeManager.getMonoFont(context);
|
||||||
|
|
||||||
|
LinearLayout item = new LinearLayout(context);
|
||||||
|
item.setOrientation(VERTICAL);
|
||||||
|
LinearLayout.LayoutParams itemParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
if (isLeft) itemParams.setMarginEnd(dpToPx(8));
|
||||||
|
else itemParams.setMarginStart(dpToPx(8));
|
||||||
|
item.setLayoutParams(itemParams);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
TextView labelView = new TextView(context);
|
||||||
|
labelView.setText(label);
|
||||||
|
labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
labelView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
labelView.setTypeface(monoFont);
|
||||||
|
labelView.setLetterSpacing(0.1f);
|
||||||
|
item.addView(labelView);
|
||||||
|
|
||||||
|
// Value
|
||||||
|
TextView valueView = new TextView(context);
|
||||||
|
valueView.setText(value);
|
||||||
|
valueView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
|
||||||
|
valueView.setTextColor(Color.parseColor("#EEFFFFFF"));
|
||||||
|
valueView.setTypeface(monoFont, android.graphics.Typeface.BOLD);
|
||||||
|
item.addView(valueView);
|
||||||
|
|
||||||
|
// Store reference
|
||||||
|
if (label.equals("CPU")) cpuValue = valueView;
|
||||||
|
else if (label.equals("RAM")) ramValue = valueView;
|
||||||
|
else if (label.equals("BATT")) batteryValue = valueView;
|
||||||
|
else if (label.equals("DISK")) storageValue = valueView;
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startUpdates() {
|
||||||
|
updateRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateStats();
|
||||||
|
handler.postDelayed(this, UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handler.post(updateRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopUpdates() {
|
||||||
|
if (handler != null && updateRunnable != null) {
|
||||||
|
handler.removeCallbacks(updateRunnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStats() {
|
||||||
|
// CPU (simulated based on running processes)
|
||||||
|
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
|
int runningApps = am.getRunningAppProcesses() != null ? am.getRunningAppProcesses().size() : 0;
|
||||||
|
int cpuPercent = Math.min(95, 15 + runningApps * 3 + (int)(Math.random() * 10));
|
||||||
|
cpuValue.setText(cpuPercent + "%");
|
||||||
|
setValueColor(cpuValue, cpuPercent);
|
||||||
|
|
||||||
|
// RAM
|
||||||
|
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
|
||||||
|
am.getMemoryInfo(memInfo);
|
||||||
|
int ramPercent = (int) (100 - (memInfo.availMem * 100 / memInfo.totalMem));
|
||||||
|
ramValue.setText(ramPercent + "%");
|
||||||
|
setValueColor(ramValue, ramPercent);
|
||||||
|
|
||||||
|
// Battery
|
||||||
|
BatteryManager bm = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);
|
||||||
|
int battery = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||||
|
batteryValue.setText(battery + "%");
|
||||||
|
setBatteryColor(batteryValue, battery);
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
java.io.File path = android.os.Environment.getDataDirectory();
|
||||||
|
android.os.StatFs stat = new android.os.StatFs(path.getPath());
|
||||||
|
long total = stat.getTotalBytes();
|
||||||
|
long free = stat.getAvailableBytes();
|
||||||
|
int storagePercent = (int) (100 - (free * 100 / total));
|
||||||
|
storageValue.setText(storagePercent + "%");
|
||||||
|
setValueColor(storageValue, storagePercent);
|
||||||
|
|
||||||
|
// Update graphs
|
||||||
|
cpuHistory[historyIndex] = cpuPercent / 100f;
|
||||||
|
ramHistory[historyIndex] = ramPercent / 100f;
|
||||||
|
historyIndex = (historyIndex + 1) % cpuHistory.length;
|
||||||
|
|
||||||
|
cpuGraph.setData(cpuHistory, historyIndex);
|
||||||
|
ramGraph.setData(ramHistory, historyIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setValueColor(TextView view, int percent) {
|
||||||
|
if (percent >= 80) view.setTextColor(Color.parseColor("#EF4444"));
|
||||||
|
else if (percent >= 60) view.setTextColor(Color.parseColor("#FBBF24"));
|
||||||
|
else view.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBatteryColor(TextView view, int percent) {
|
||||||
|
if (percent <= 20) view.setTextColor(Color.parseColor("#EF4444"));
|
||||||
|
else if (percent <= 40) view.setTextColor(Color.parseColor("#FBBF24"));
|
||||||
|
else view.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * context.getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Mini Graph View
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static class GraphView extends View {
|
||||||
|
private Paint linePaint;
|
||||||
|
private Paint fillPaint;
|
||||||
|
private Paint labelPaint;
|
||||||
|
private Path path;
|
||||||
|
private float[] data;
|
||||||
|
private int startIndex;
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
public GraphView(Context context, int color, String label) {
|
||||||
|
super(context);
|
||||||
|
this.label = label;
|
||||||
|
|
||||||
|
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
linePaint.setColor(color);
|
||||||
|
linePaint.setStrokeWidth(2f);
|
||||||
|
linePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
|
||||||
|
fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
fillPaint.setColor(Color.argb(40, Color.red(color), Color.green(color), Color.blue(color)));
|
||||||
|
fillPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
labelPaint.setColor(Color.argb(100, 255, 255, 255));
|
||||||
|
labelPaint.setTextSize(18f);
|
||||||
|
|
||||||
|
path = new Path();
|
||||||
|
data = new float[20];
|
||||||
|
|
||||||
|
// Background
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(8f);
|
||||||
|
bg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
setBackground(bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(float[] history, int index) {
|
||||||
|
this.data = history.clone();
|
||||||
|
this.startIndex = index;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
int w = getWidth();
|
||||||
|
int h = getHeight();
|
||||||
|
if (w == 0 || h == 0 || data == null) return;
|
||||||
|
|
||||||
|
float segmentWidth = (float) w / (data.length - 1);
|
||||||
|
float padding = 4f;
|
||||||
|
|
||||||
|
// Build path
|
||||||
|
path.reset();
|
||||||
|
boolean started = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
int idx = (startIndex + i) % data.length;
|
||||||
|
float x = i * segmentWidth;
|
||||||
|
float y = h - padding - (data[idx] * (h - padding * 2));
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
path.moveTo(x, y);
|
||||||
|
started = true;
|
||||||
|
} else {
|
||||||
|
path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill
|
||||||
|
Path fillPath = new Path(path);
|
||||||
|
fillPath.lineTo(w, h);
|
||||||
|
fillPath.lineTo(0, h);
|
||||||
|
fillPath.close();
|
||||||
|
canvas.drawPath(fillPath, fillPaint);
|
||||||
|
|
||||||
|
// Line
|
||||||
|
canvas.drawPath(path, linePaint);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
canvas.drawText(label, padding + 2, 14, labelPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
352
android/app/src/main/java/com/aethex/os/FileManagerActivity.java
Normal file
352
android/app/src/main/java/com/aethex/os/FileManagerActivity.java
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Stack;
|
||||||
|
|
||||||
|
public class FileManagerActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private TextView breadcrumb;
|
||||||
|
private FileAdapter adapter;
|
||||||
|
|
||||||
|
private final Stack<String> pathStack = new Stack<>();
|
||||||
|
private String currentPath = "/home";
|
||||||
|
|
||||||
|
// Virtual file system
|
||||||
|
private final Map<String, List<FileEntry>> fileSystem = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_file_manager);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
breadcrumb = findViewById(R.id.fm_breadcrumb);
|
||||||
|
recyclerView = findViewById(R.id.fm_recycler);
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
findViewById(R.id.fm_back).setOnClickListener(v -> {
|
||||||
|
if (!navigateUp()) {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Storage bar - 42.3 / 128 = ~33%
|
||||||
|
View storageBar = findViewById(R.id.fm_storage_bar);
|
||||||
|
storageBar.post(() -> {
|
||||||
|
ViewGroup.LayoutParams params = storageBar.getLayoutParams();
|
||||||
|
int parentWidth = ((View) storageBar.getParent()).getWidth();
|
||||||
|
params.width = (int) (parentWidth * 0.33f);
|
||||||
|
storageBar.setLayoutParams(params);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build virtual file system
|
||||||
|
buildFileSystem();
|
||||||
|
|
||||||
|
// Setup RecyclerView
|
||||||
|
adapter = new FileAdapter();
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
// Load root
|
||||||
|
navigateTo(currentPath);
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View root = findViewById(R.id.file_manager_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildFileSystem() {
|
||||||
|
// /home - top-level folders
|
||||||
|
List<FileEntry> home = new ArrayList<>();
|
||||||
|
home.add(new FileEntry("Documents", true, "--", "2026-01-15"));
|
||||||
|
home.add(new FileEntry("Downloads", true, "--", "2026-02-10"));
|
||||||
|
home.add(new FileEntry("Pictures", true, "--", "2026-02-08"));
|
||||||
|
home.add(new FileEntry("Music", true, "--", "2026-01-22"));
|
||||||
|
home.add(new FileEntry("Videos", true, "--", "2026-02-01"));
|
||||||
|
home.add(new FileEntry("System", true, "--", "2025-12-01"));
|
||||||
|
home.add(new FileEntry("Programs", true, "--", "2026-01-30"));
|
||||||
|
fileSystem.put("/home", home);
|
||||||
|
|
||||||
|
// /home/Documents
|
||||||
|
List<FileEntry> docs = new ArrayList<>();
|
||||||
|
docs.add(new FileEntry("report.pdf", false, "2.4 MB", "2026-01-14"));
|
||||||
|
docs.add(new FileEntry("notes.txt", false, "12 KB", "2026-01-10"));
|
||||||
|
docs.add(new FileEntry("budget.xlsx", false, "890 KB", "2025-12-20"));
|
||||||
|
docs.add(new FileEntry("resume.docx", false, "156 KB", "2026-01-05"));
|
||||||
|
fileSystem.put("/home/Documents", docs);
|
||||||
|
|
||||||
|
// /home/Downloads
|
||||||
|
List<FileEntry> downloads = new ArrayList<>();
|
||||||
|
downloads.add(new FileEntry("setup.exe", false, "45.2 MB", "2026-02-09"));
|
||||||
|
downloads.add(new FileEntry("image_001.png", false, "3.1 MB", "2026-02-08"));
|
||||||
|
downloads.add(new FileEntry("song.mp3", false, "8.7 MB", "2026-02-05"));
|
||||||
|
downloads.add(new FileEntry("archive.zip", false, "22.0 MB", "2026-01-28"));
|
||||||
|
fileSystem.put("/home/Downloads", downloads);
|
||||||
|
|
||||||
|
// /home/Pictures
|
||||||
|
List<FileEntry> pictures = new ArrayList<>();
|
||||||
|
pictures.add(new FileEntry("vacation.jpg", false, "4.2 MB", "2026-02-07"));
|
||||||
|
pictures.add(new FileEntry("profile.png", false, "1.1 MB", "2026-01-18"));
|
||||||
|
pictures.add(new FileEntry("screenshot.png", false, "2.8 MB", "2026-02-03"));
|
||||||
|
pictures.add(new FileEntry("wallpaper.jpg", false, "5.6 MB", "2025-11-12"));
|
||||||
|
fileSystem.put("/home/Pictures", pictures);
|
||||||
|
|
||||||
|
// /home/Music
|
||||||
|
List<FileEntry> music = new ArrayList<>();
|
||||||
|
music.add(new FileEntry("track01.mp3", false, "7.2 MB", "2025-10-15"));
|
||||||
|
music.add(new FileEntry("album.flac", false, "42.1 MB", "2025-09-20"));
|
||||||
|
music.add(new FileEntry("podcast.mp3", false, "28.4 MB", "2026-01-22"));
|
||||||
|
fileSystem.put("/home/Music", music);
|
||||||
|
|
||||||
|
// /home/Videos
|
||||||
|
List<FileEntry> videos = new ArrayList<>();
|
||||||
|
videos.add(new FileEntry("tutorial.mp4", false, "156.0 MB", "2026-01-30"));
|
||||||
|
videos.add(new FileEntry("clip.mov", false, "23.4 MB", "2025-12-14"));
|
||||||
|
fileSystem.put("/home/Videos", videos);
|
||||||
|
|
||||||
|
// /home/System
|
||||||
|
List<FileEntry> system = new ArrayList<>();
|
||||||
|
system.add(new FileEntry("config.sys", false, "4 KB", "2025-12-01"));
|
||||||
|
system.add(new FileEntry("kernel.log", false, "128 KB", "2026-02-14"));
|
||||||
|
system.add(new FileEntry("boot.ini", false, "2 KB", "2025-11-01"));
|
||||||
|
fileSystem.put("/home/System", system);
|
||||||
|
|
||||||
|
// /home/Programs
|
||||||
|
List<FileEntry> programs = new ArrayList<>();
|
||||||
|
programs.add(new FileEntry("terminal.app", false, "12.0 MB", "2026-01-30"));
|
||||||
|
programs.add(new FileEntry("calculator.app", false, "8.4 MB", "2026-01-30"));
|
||||||
|
programs.add(new FileEntry("browser.app", false, "34.2 MB", "2026-01-25"));
|
||||||
|
fileSystem.put("/home/Programs", programs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void navigateTo(String path) {
|
||||||
|
currentPath = path;
|
||||||
|
breadcrumb.setText(currentPath);
|
||||||
|
|
||||||
|
List<FileEntry> entries = fileSystem.get(path);
|
||||||
|
if (entries == null) {
|
||||||
|
entries = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort folders first, then by name
|
||||||
|
List<FileEntry> sorted = new ArrayList<>(entries);
|
||||||
|
Collections.sort(sorted, (a, b) -> {
|
||||||
|
if (a.isFolder && !b.isFolder) return -1;
|
||||||
|
if (!a.isFolder && b.isFolder) return 1;
|
||||||
|
return a.name.compareToIgnoreCase(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
adapter.setData(sorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean navigateUp() {
|
||||||
|
if (!pathStack.isEmpty()) {
|
||||||
|
currentPath = pathStack.pop();
|
||||||
|
navigateTo(currentPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!currentPath.equals("/home")) {
|
||||||
|
int lastSlash = currentPath.lastIndexOf('/');
|
||||||
|
if (lastSlash > 0) {
|
||||||
|
currentPath = currentPath.substring(0, lastSlash);
|
||||||
|
navigateTo(currentPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (!navigateUp()) {
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── File entry model ───────────────────────────────────────────
|
||||||
|
|
||||||
|
private static class FileEntry {
|
||||||
|
final String name;
|
||||||
|
final boolean isFolder;
|
||||||
|
final String size;
|
||||||
|
final String date;
|
||||||
|
|
||||||
|
FileEntry(String name, boolean isFolder, String size, String date) {
|
||||||
|
this.name = name;
|
||||||
|
this.isFolder = isFolder;
|
||||||
|
this.size = size;
|
||||||
|
this.date = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── RecyclerView adapter ───────────────────────────────────────
|
||||||
|
|
||||||
|
private class FileAdapter extends RecyclerView.Adapter<FileAdapter.FileViewHolder> {
|
||||||
|
|
||||||
|
private List<FileEntry> items = new ArrayList<>();
|
||||||
|
|
||||||
|
void setData(List<FileEntry> data) {
|
||||||
|
this.items = data;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public FileViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_file_entry, parent, false);
|
||||||
|
return new FileViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull FileViewHolder holder, int position) {
|
||||||
|
FileEntry entry = items.get(position);
|
||||||
|
holder.bind(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
final TextView icon;
|
||||||
|
final TextView name;
|
||||||
|
final TextView size;
|
||||||
|
final TextView date;
|
||||||
|
|
||||||
|
FileViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
icon = itemView.findViewById(R.id.file_icon);
|
||||||
|
name = itemView.findViewById(R.id.file_name);
|
||||||
|
size = itemView.findViewById(R.id.file_size);
|
||||||
|
date = itemView.findViewById(R.id.file_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(FileEntry entry) {
|
||||||
|
if (entry.isFolder) {
|
||||||
|
icon.setText("\u25B8");
|
||||||
|
icon.setTextColor(themeManager.getPrimaryColor(FileManagerActivity.this));
|
||||||
|
name.setTextColor(themeManager.getPrimaryColor(FileManagerActivity.this));
|
||||||
|
} else {
|
||||||
|
// Determine icon based on file extension
|
||||||
|
String ext = getExtension(entry.name);
|
||||||
|
icon.setText(getFileIcon(ext));
|
||||||
|
icon.setTextColor(getResources().getColor(R.color.text_white_40));
|
||||||
|
name.setTextColor(getResources().getColor(R.color.text_white_60));
|
||||||
|
}
|
||||||
|
|
||||||
|
name.setText(entry.isFolder ? entry.name + "/" : entry.name);
|
||||||
|
size.setText(entry.size);
|
||||||
|
date.setText(entry.date);
|
||||||
|
|
||||||
|
itemView.setOnClickListener(v -> {
|
||||||
|
if (entry.isFolder) {
|
||||||
|
// Subtle press animation
|
||||||
|
v.animate().alpha(0.6f).setDuration(60).withEndAction(() ->
|
||||||
|
v.animate().alpha(1f).setDuration(60).withEndAction(() -> {
|
||||||
|
pathStack.push(currentPath);
|
||||||
|
navigateTo(currentPath + "/" + entry.name);
|
||||||
|
}).start()
|
||||||
|
).start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getExtension(String filename) {
|
||||||
|
int dot = filename.lastIndexOf('.');
|
||||||
|
if (dot >= 0) {
|
||||||
|
return filename.substring(dot + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileIcon(String ext) {
|
||||||
|
switch (ext) {
|
||||||
|
case "pdf":
|
||||||
|
case "docx":
|
||||||
|
case "doc":
|
||||||
|
case "txt":
|
||||||
|
case "xlsx":
|
||||||
|
return "\u2637"; // document-like
|
||||||
|
case "png":
|
||||||
|
case "jpg":
|
||||||
|
case "jpeg":
|
||||||
|
case "bmp":
|
||||||
|
return "\u25A3"; // image-like square
|
||||||
|
case "mp3":
|
||||||
|
case "flac":
|
||||||
|
case "wav":
|
||||||
|
return "\u266B"; // music note
|
||||||
|
case "mp4":
|
||||||
|
case "mov":
|
||||||
|
case "avi":
|
||||||
|
return "\u25B6"; // play triangle
|
||||||
|
case "exe":
|
||||||
|
case "app":
|
||||||
|
return "\u2B22"; // hexagon
|
||||||
|
case "zip":
|
||||||
|
case "tar":
|
||||||
|
case "gz":
|
||||||
|
return "\u29C9"; // stacked squares
|
||||||
|
case "sys":
|
||||||
|
case "ini":
|
||||||
|
case "log":
|
||||||
|
case "cfg":
|
||||||
|
return "\u2699"; // gear
|
||||||
|
default:
|
||||||
|
return "\u25A1"; // empty square
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── System UI ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
536
android/app/src/main/java/com/aethex/os/MailActivity.java
Normal file
536
android/app/src/main/java/com/aethex/os/MailActivity.java
Normal file
|
|
@ -0,0 +1,536 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class MailActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private LinearLayout mailList;
|
||||||
|
private LinearLayout composePanel;
|
||||||
|
private FrameLayout detailOverlay;
|
||||||
|
private boolean composeOpen = false;
|
||||||
|
private Typeface monoFont;
|
||||||
|
private Typeface displayFont;
|
||||||
|
|
||||||
|
private static final String[][] EMAILS = {
|
||||||
|
{"System", "system@aethex.os", "Welcome to AeThex OS", "Welcome, Architect.\n\nYour AeThex OS installation is complete. All subsystems are online and operating within normal parameters.\n\nYour clearance level has been set to Foundation. You may change this at any time via Settings or the Start Menu.\n\nStay vigilant.\n\n— AeThex System", "just now"},
|
||||||
|
{"Security", "sec@aethex.os", "Security Scan Complete", "Daily security scan completed.\n\nThreats detected: 0\nVulnerabilities patched: 3\nFirewall status: ACTIVE\nEncryption: AES-256\n\nAll modules passed integrity checks. Next scan scheduled in 24 hours.", "5m ago"},
|
||||||
|
{"Updates", "updates@aethex.os", "Module Update Available", "A new update is available for the following modules:\n\n• Terminal v2.1 — New commands added\n• Browser v1.3 — Performance improvements\n• Snake v1.2 — Leaderboard support\n\nUpdates will be applied automatically on next reboot.", "12m ago"},
|
||||||
|
{"Projects", "projects@aethex.os", "Sprint Review Reminder", "Reminder: Sprint review for AeThex Mobile is scheduled for tomorrow at 14:00.\n\nPlease prepare your status updates and demo materials.\n\nCurrent sprint velocity: 42 points\nBurn-down: On track", "1h ago"},
|
||||||
|
{"Analytics", "analytics@aethex.os", "Weekly Report", "Weekly analytics summary:\n\n• Active sessions: 1,247\n• Uptime: 99.97%\n• CPU avg: 18%\n• Memory usage: 62%\n• Top app: Terminal (347 opens)\n\nFull report available in the Analytics module.", "3h ago"},
|
||||||
|
{"Marketplace", "market@aethex.os", "New Modules Available", "New modules have been published to the marketplace:\n\n1. Dark Matter Theme Pack — 500 credits\n2. Neural Network Toolkit — 1,200 credits\n3. Advanced Encryption Suite — 800 credits\n\nBrowse the marketplace to learn more.", "6h ago"},
|
||||||
|
{"HR", "hr@aethex.os", "Access Level Review", "Your access level is due for review.\n\nCurrent level: 5\nNext review date: 2025-03-01\n\nPlease ensure your Passport information is up to date before the review period.", "1d ago"},
|
||||||
|
{"Archive", "archive@aethex.os", "Backup Complete", "System backup completed successfully.\n\nBackup size: 2.4 GB\nDuration: 4m 32s\nStorage used: 34%\n\nNext automatic backup: 7 days.", "2d ago"},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
monoFont = themeManager.getMonoFont(this);
|
||||||
|
displayFont = themeManager.getDisplayFont(this);
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
TextView title = findViewById(R.id.app_title);
|
||||||
|
title.setText("Mail");
|
||||||
|
TextView nameDisplay = findViewById(R.id.app_name_display);
|
||||||
|
|
||||||
|
findViewById(R.id.app_back).setOnClickListener(v -> {
|
||||||
|
if (composeOpen) {
|
||||||
|
closeCompose();
|
||||||
|
} else {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout content = (LinearLayout) nameDisplay.getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
content.setGravity(Gravity.TOP);
|
||||||
|
content.setPadding(0, 0, 0, 0);
|
||||||
|
buildMailUI(content);
|
||||||
|
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
AeThexKeyboard.attach(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildMailUI(LinearLayout parent) {
|
||||||
|
// Header bar with inbox count + compose button
|
||||||
|
LinearLayout header = new LinearLayout(this);
|
||||||
|
header.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
header.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
header.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(12));
|
||||||
|
|
||||||
|
TextView inboxLabel = new TextView(this);
|
||||||
|
inboxLabel.setText("INBOX");
|
||||||
|
inboxLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
inboxLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
inboxLabel.setTypeface(monoFont);
|
||||||
|
inboxLabel.setLetterSpacing(0.15f);
|
||||||
|
header.addView(inboxLabel);
|
||||||
|
|
||||||
|
// Unread count badge
|
||||||
|
TextView unreadBadge = new TextView(this);
|
||||||
|
unreadBadge.setText(String.valueOf(EMAILS.length));
|
||||||
|
unreadBadge.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
unreadBadge.setTextColor(Color.WHITE);
|
||||||
|
unreadBadge.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
unreadBadge.setGravity(Gravity.CENTER);
|
||||||
|
GradientDrawable badgeBg = new GradientDrawable();
|
||||||
|
badgeBg.setShape(GradientDrawable.OVAL);
|
||||||
|
badgeBg.setColor(themeManager.getPrimaryColor(this));
|
||||||
|
unreadBadge.setBackground(badgeBg);
|
||||||
|
LinearLayout.LayoutParams badgeP = new LinearLayout.LayoutParams(dpToPx(20), dpToPx(20));
|
||||||
|
badgeP.setMarginStart(dpToPx(8));
|
||||||
|
unreadBadge.setLayoutParams(badgeP);
|
||||||
|
header.addView(unreadBadge);
|
||||||
|
|
||||||
|
// Spacer
|
||||||
|
View spacer = new View(this);
|
||||||
|
spacer.setLayoutParams(new LinearLayout.LayoutParams(0, 1, 1f));
|
||||||
|
header.addView(spacer);
|
||||||
|
|
||||||
|
// Compose button
|
||||||
|
TextView composeBtn = new TextView(this);
|
||||||
|
composeBtn.setText("+ Compose");
|
||||||
|
composeBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
composeBtn.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
composeBtn.setTypeface(monoFont);
|
||||||
|
composeBtn.setPadding(dpToPx(12), dpToPx(6), dpToPx(12), dpToPx(6));
|
||||||
|
GradientDrawable composeBg = new GradientDrawable();
|
||||||
|
composeBg.setCornerRadius(dpToPx(8));
|
||||||
|
composeBg.setStroke(dpToPx(1), themeManager.getPrimaryColor(this));
|
||||||
|
composeBg.setColor(Color.TRANSPARENT);
|
||||||
|
composeBtn.setBackground(composeBg);
|
||||||
|
composeBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
showCompose();
|
||||||
|
});
|
||||||
|
header.addView(composeBtn);
|
||||||
|
|
||||||
|
parent.addView(header);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
View div = new View(this);
|
||||||
|
div.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(1)));
|
||||||
|
div.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
parent.addView(div);
|
||||||
|
|
||||||
|
// Mail list in scroll
|
||||||
|
ScrollView scroll = new ScrollView(this);
|
||||||
|
scroll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
scroll.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
|
||||||
|
mailList = new LinearLayout(this);
|
||||||
|
mailList.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mailList.setPadding(dpToPx(8), dpToPx(4), dpToPx(8), dpToPx(8));
|
||||||
|
scroll.addView(mailList);
|
||||||
|
parent.addView(scroll);
|
||||||
|
|
||||||
|
populateMailList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateMailList() {
|
||||||
|
mailList.removeAllViews();
|
||||||
|
for (int i = 0; i < EMAILS.length; i++) {
|
||||||
|
final String[] email = EMAILS[i];
|
||||||
|
LinearLayout row = new LinearLayout(this);
|
||||||
|
row.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
row.setPadding(dpToPx(14), dpToPx(12), dpToPx(14), dpToPx(12));
|
||||||
|
LinearLayout.LayoutParams rowP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rowP.bottomMargin = dpToPx(4);
|
||||||
|
row.setLayoutParams(rowP);
|
||||||
|
|
||||||
|
GradientDrawable rowBg = new GradientDrawable();
|
||||||
|
rowBg.setCornerRadius(dpToPx(10));
|
||||||
|
rowBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
row.setBackground(rowBg);
|
||||||
|
|
||||||
|
// Top: sender + time
|
||||||
|
LinearLayout topRow = new LinearLayout(this);
|
||||||
|
topRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
topRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
// Sender avatar dot
|
||||||
|
View dot = new View(this);
|
||||||
|
GradientDrawable dotBg = new GradientDrawable();
|
||||||
|
dotBg.setShape(GradientDrawable.OVAL);
|
||||||
|
int dotColor = i < 3 ? themeManager.getPrimaryColor(this) : Color.parseColor("#66FFFFFF");
|
||||||
|
dotBg.setColor(dotColor);
|
||||||
|
dot.setBackground(dotBg);
|
||||||
|
LinearLayout.LayoutParams dotP = new LinearLayout.LayoutParams(dpToPx(8), dpToPx(8));
|
||||||
|
dotP.setMarginEnd(dpToPx(8));
|
||||||
|
dot.setLayoutParams(dotP);
|
||||||
|
topRow.addView(dot);
|
||||||
|
|
||||||
|
TextView sender = new TextView(this);
|
||||||
|
sender.setText(email[0]);
|
||||||
|
sender.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
sender.setTextColor(i < 3 ? Color.WHITE : Color.parseColor("#99FFFFFF"));
|
||||||
|
sender.setTypeface(monoFont, i < 3 ? Typeface.BOLD : Typeface.NORMAL);
|
||||||
|
sender.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
topRow.addView(sender);
|
||||||
|
|
||||||
|
TextView time = new TextView(this);
|
||||||
|
time.setText(email[4]);
|
||||||
|
time.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
time.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
time.setTypeface(monoFont);
|
||||||
|
topRow.addView(time);
|
||||||
|
|
||||||
|
row.addView(topRow);
|
||||||
|
|
||||||
|
// Subject
|
||||||
|
TextView subject = new TextView(this);
|
||||||
|
subject.setText(email[2]);
|
||||||
|
subject.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
subject.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
subject.setTypeface(displayFont);
|
||||||
|
subject.setSingleLine(true);
|
||||||
|
LinearLayout.LayoutParams subP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
subP.topMargin = dpToPx(4);
|
||||||
|
subject.setLayoutParams(subP);
|
||||||
|
row.addView(subject);
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
TextView preview = new TextView(this);
|
||||||
|
String previewText = email[3].replace("\n", " ");
|
||||||
|
if (previewText.length() > 80) previewText = previewText.substring(0, 80) + "...";
|
||||||
|
preview.setText(previewText);
|
||||||
|
preview.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
preview.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
preview.setTypeface(monoFont);
|
||||||
|
preview.setSingleLine(true);
|
||||||
|
LinearLayout.LayoutParams prevP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
prevP.topMargin = dpToPx(2);
|
||||||
|
preview.setLayoutParams(prevP);
|
||||||
|
row.addView(preview);
|
||||||
|
|
||||||
|
row.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
showEmailDetail(email);
|
||||||
|
});
|
||||||
|
|
||||||
|
mailList.addView(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEmailDetail(String[] email) {
|
||||||
|
FrameLayout root = (FrameLayout) findViewById(R.id.app_root);
|
||||||
|
|
||||||
|
detailOverlay = new FrameLayout(this);
|
||||||
|
detailOverlay.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
detailOverlay.setBackgroundColor(Color.parseColor("#F2080810"));
|
||||||
|
|
||||||
|
LinearLayout detail = new LinearLayout(this);
|
||||||
|
detail.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
detail.setPadding(dpToPx(20), dpToPx(20), dpToPx(20), dpToPx(20));
|
||||||
|
FrameLayout.LayoutParams dp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
detail.setLayoutParams(dp);
|
||||||
|
|
||||||
|
// Close row
|
||||||
|
LinearLayout closeRow = new LinearLayout(this);
|
||||||
|
closeRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
TextView backBtn = new TextView(this);
|
||||||
|
backBtn.setText("← Back");
|
||||||
|
backBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
backBtn.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
backBtn.setTypeface(monoFont);
|
||||||
|
backBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
root.removeView(detailOverlay);
|
||||||
|
detailOverlay = null;
|
||||||
|
});
|
||||||
|
closeRow.addView(backBtn);
|
||||||
|
detail.addView(closeRow);
|
||||||
|
|
||||||
|
// Subject
|
||||||
|
TextView subject = new TextView(this);
|
||||||
|
subject.setText(email[2]);
|
||||||
|
subject.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||||
|
subject.setTextColor(Color.WHITE);
|
||||||
|
subject.setTypeface(displayFont);
|
||||||
|
LinearLayout.LayoutParams sP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
sP.topMargin = dpToPx(16);
|
||||||
|
subject.setLayoutParams(sP);
|
||||||
|
detail.addView(subject);
|
||||||
|
|
||||||
|
// From line
|
||||||
|
TextView from = new TextView(this);
|
||||||
|
from.setText("From: " + email[0] + " <" + email[1] + ">");
|
||||||
|
from.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
from.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
from.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams fP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
fP.topMargin = dpToPx(8);
|
||||||
|
from.setLayoutParams(fP);
|
||||||
|
detail.addView(from);
|
||||||
|
|
||||||
|
// Time
|
||||||
|
TextView time = new TextView(this);
|
||||||
|
time.setText(email[4]);
|
||||||
|
time.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
time.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
time.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams tP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
tP.topMargin = dpToPx(2);
|
||||||
|
time.setLayoutParams(tP);
|
||||||
|
detail.addView(time);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
View div = new View(this);
|
||||||
|
LinearLayout.LayoutParams divP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(1));
|
||||||
|
divP.topMargin = dpToPx(16);
|
||||||
|
divP.bottomMargin = dpToPx(16);
|
||||||
|
div.setLayoutParams(divP);
|
||||||
|
div.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
detail.addView(div);
|
||||||
|
|
||||||
|
// Body in scroll
|
||||||
|
ScrollView bodyScroll = new ScrollView(this);
|
||||||
|
bodyScroll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
|
||||||
|
TextView body = new TextView(this);
|
||||||
|
body.setText(email[3]);
|
||||||
|
body.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
body.setTextColor(Color.parseColor("#B3FFFFFF"));
|
||||||
|
body.setTypeface(monoFont);
|
||||||
|
body.setLineSpacing(dpToPx(4), 1f);
|
||||||
|
bodyScroll.addView(body);
|
||||||
|
detail.addView(bodyScroll);
|
||||||
|
|
||||||
|
// Reply button
|
||||||
|
TextView replyBtn = new TextView(this);
|
||||||
|
replyBtn.setText("Reply");
|
||||||
|
replyBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
replyBtn.setTextColor(Color.WHITE);
|
||||||
|
replyBtn.setTypeface(monoFont);
|
||||||
|
replyBtn.setGravity(Gravity.CENTER);
|
||||||
|
replyBtn.setPadding(0, dpToPx(12), 0, dpToPx(12));
|
||||||
|
LinearLayout.LayoutParams rP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rP.topMargin = dpToPx(12);
|
||||||
|
replyBtn.setLayoutParams(rP);
|
||||||
|
GradientDrawable replyBg = new GradientDrawable();
|
||||||
|
replyBg.setCornerRadius(dpToPx(10));
|
||||||
|
replyBg.setColor(themeManager.getPrimaryColor(this));
|
||||||
|
replyBtn.setBackground(replyBg);
|
||||||
|
replyBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
root.removeView(detailOverlay);
|
||||||
|
detailOverlay = null;
|
||||||
|
showCompose();
|
||||||
|
});
|
||||||
|
detail.addView(replyBtn);
|
||||||
|
|
||||||
|
detailOverlay.addView(detail);
|
||||||
|
|
||||||
|
detailOverlay.setAlpha(0f);
|
||||||
|
root.addView(detailOverlay);
|
||||||
|
detailOverlay.animate().alpha(1f).setDuration(200).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCompose() {
|
||||||
|
composeOpen = true;
|
||||||
|
FrameLayout root = (FrameLayout) findViewById(R.id.app_root);
|
||||||
|
|
||||||
|
composePanel = new LinearLayout(this);
|
||||||
|
composePanel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
composePanel.setTag("compose_panel");
|
||||||
|
composePanel.setBackgroundColor(Color.parseColor("#F2080810"));
|
||||||
|
composePanel.setPadding(dpToPx(20), dpToPx(20), dpToPx(20), dpToPx(20));
|
||||||
|
FrameLayout.LayoutParams cp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
composePanel.setLayoutParams(cp);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
LinearLayout hdr = new LinearLayout(this);
|
||||||
|
hdr.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
TextView cancel = new TextView(this);
|
||||||
|
cancel.setText("Cancel");
|
||||||
|
cancel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
cancel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
cancel.setTypeface(monoFont);
|
||||||
|
cancel.setOnClickListener(v -> closeCompose());
|
||||||
|
hdr.addView(cancel);
|
||||||
|
|
||||||
|
View sp = new View(this);
|
||||||
|
sp.setLayoutParams(new LinearLayout.LayoutParams(0, 1, 1f));
|
||||||
|
hdr.addView(sp);
|
||||||
|
|
||||||
|
TextView sendBtn = new TextView(this);
|
||||||
|
sendBtn.setText("Send");
|
||||||
|
sendBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
sendBtn.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
sendBtn.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
sendBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
closeCompose();
|
||||||
|
AeThexToast.show(this, "Message sent", AeThexToast.Type.SUCCESS);
|
||||||
|
});
|
||||||
|
hdr.addView(sendBtn);
|
||||||
|
composePanel.addView(hdr);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
TextView compTitle = new TextView(this);
|
||||||
|
compTitle.setText("NEW MESSAGE");
|
||||||
|
compTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
compTitle.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
compTitle.setTypeface(monoFont);
|
||||||
|
compTitle.setLetterSpacing(0.15f);
|
||||||
|
LinearLayout.LayoutParams ctP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
ctP.topMargin = dpToPx(16);
|
||||||
|
ctP.bottomMargin = dpToPx(12);
|
||||||
|
compTitle.setLayoutParams(ctP);
|
||||||
|
composePanel.addView(compTitle);
|
||||||
|
|
||||||
|
addComposeField(composePanel, "To");
|
||||||
|
addComposeField(composePanel, "Subject");
|
||||||
|
|
||||||
|
// Body field
|
||||||
|
EditText bodyField = new EditText(this);
|
||||||
|
bodyField.setHint("Message body...");
|
||||||
|
bodyField.setHintTextColor(Color.parseColor("#33FFFFFF"));
|
||||||
|
bodyField.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
bodyField.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
bodyField.setTypeface(monoFont);
|
||||||
|
bodyField.setMinLines(8);
|
||||||
|
bodyField.setGravity(Gravity.TOP);
|
||||||
|
bodyField.setBackgroundColor(Color.TRANSPARENT);
|
||||||
|
bodyField.setPadding(dpToPx(14), dpToPx(12), dpToPx(14), dpToPx(12));
|
||||||
|
LinearLayout.LayoutParams bfP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f);
|
||||||
|
bfP.topMargin = dpToPx(8);
|
||||||
|
bodyField.setLayoutParams(bfP);
|
||||||
|
|
||||||
|
GradientDrawable fieldBg = new GradientDrawable();
|
||||||
|
fieldBg.setCornerRadius(dpToPx(10));
|
||||||
|
fieldBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
fieldBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
bodyField.setBackground(fieldBg);
|
||||||
|
composePanel.addView(bodyField);
|
||||||
|
|
||||||
|
composePanel.setAlpha(0f);
|
||||||
|
composePanel.setTranslationY(dpToPx(40));
|
||||||
|
root.addView(composePanel);
|
||||||
|
composePanel.animate().alpha(1f).translationY(0f).setDuration(250).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addComposeField(LinearLayout parent, String hint) {
|
||||||
|
EditText field = new EditText(this);
|
||||||
|
field.setHint(hint);
|
||||||
|
field.setHintTextColor(Color.parseColor("#33FFFFFF"));
|
||||||
|
field.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
field.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
field.setTypeface(monoFont);
|
||||||
|
field.setSingleLine(true);
|
||||||
|
field.setPadding(dpToPx(14), dpToPx(10), dpToPx(14), dpToPx(10));
|
||||||
|
LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
p.bottomMargin = dpToPx(6);
|
||||||
|
field.setLayoutParams(p);
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(10));
|
||||||
|
bg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
field.setBackground(bg);
|
||||||
|
parent.addView(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeCompose() {
|
||||||
|
composeOpen = false;
|
||||||
|
if (composePanel != null) {
|
||||||
|
FrameLayout root = (FrameLayout) findViewById(R.id.app_root);
|
||||||
|
composePanel.animate().alpha(0f).translationY(dpToPx(40)).setDuration(200).withEndAction(() -> {
|
||||||
|
root.removeView(composePanel);
|
||||||
|
composePanel = null;
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
AeThexKeyboard.dismissKeyboard(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (composeOpen) {
|
||||||
|
closeCompose();
|
||||||
|
} else if (detailOverlay != null) {
|
||||||
|
FrameLayout root = (FrameLayout) findViewById(R.id.app_root);
|
||||||
|
root.removeView(detailOverlay);
|
||||||
|
detailOverlay = null;
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,540 @@
|
||||||
package com.aethex.os;
|
package com.aethex.os;
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
|
import android.view.animation.LinearInterpolator;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
/**
|
||||||
// Reverting to standard Capacitor implementation.
|
* AeThexOS Boot Screen — 5-phase cinematic boot sequence.
|
||||||
// The BridgeActivity handles the WebView creation and loading of assets
|
*
|
||||||
// via the scheme configured in capacitor.config.ts (androidScheme: "https").
|
* Phase 1: Hardware scan (fast scrolling log lines)
|
||||||
|
* Phase 2: Security check (threat level assessment)
|
||||||
|
* Phase 3: Biometric simulation (fingerprint scan animation)
|
||||||
|
* Phase 4: System init (loading services)
|
||||||
|
* Phase 5: Desktop transition
|
||||||
|
*/
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
// Track whether boot has already played this app session
|
||||||
|
private static boolean hasBooted = false;
|
||||||
|
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
private TextView percentText;
|
||||||
|
private TextView statusText;
|
||||||
|
private TextView logText;
|
||||||
|
private ScrollView logScroll;
|
||||||
|
private View glowView;
|
||||||
|
private Handler handler;
|
||||||
|
private int progress = 0;
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
|
||||||
|
// Phase 2: Threat level
|
||||||
|
private TextView threatLabel;
|
||||||
|
private View threatDot;
|
||||||
|
|
||||||
|
// Phase 3: Biometric
|
||||||
|
private FrameLayout biometricContainer;
|
||||||
|
private TextView biometricIcon;
|
||||||
|
private TextView biometricStatus;
|
||||||
|
|
||||||
|
private int currentPhase = 0;
|
||||||
|
|
||||||
|
// ── Phase 1: Hardware Scan messages ──
|
||||||
|
private static final String[] PHASE1_MSGS = {
|
||||||
|
"[KERN] Loading AeThex kernel modules...",
|
||||||
|
"[KERN] Memory check: 8192MB OK",
|
||||||
|
"[KERN] CPU cores initialized: 8",
|
||||||
|
"[KERN] Interrupt handlers registered",
|
||||||
|
"[SYS] Mounting filesystem /dev/aethex0...",
|
||||||
|
"[SYS] Filesystem mounted OK",
|
||||||
|
"[GPU] Graphics pipeline initialized",
|
||||||
|
"[GPU] Display resolution: adaptive",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Phase 2: Security Check messages ──
|
||||||
|
private static final String[] PHASE2_MSGS = {
|
||||||
|
"[SEC] Initializing security protocols...",
|
||||||
|
"[SEC] Scanning network interfaces...",
|
||||||
|
"[SEC] Firewall rules loaded: 247 active",
|
||||||
|
"[SEC] Intrusion detection system: ARMED",
|
||||||
|
"[SEC] Encryption engine: AES-256-GCM",
|
||||||
|
"[SEC] Running threat assessment...",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Phase 4: System Init messages ──
|
||||||
|
private static final String[] PHASE4_MSGS = {
|
||||||
|
"[NET] WiFi adapter detected",
|
||||||
|
"[NET] Establishing secure tunnel...",
|
||||||
|
"[SVC] Starting system services...",
|
||||||
|
"[SVC] Notification daemon started",
|
||||||
|
"[SVC] Window compositor ready",
|
||||||
|
"[APP] Loading application registry...",
|
||||||
|
"[APP] 20 applications found",
|
||||||
|
"[SYS] Loading user preferences...",
|
||||||
|
"[SYS] Theme engine initialized",
|
||||||
|
"[SYS] Desktop environment ready",
|
||||||
|
"[BOOT] All systems nominal",
|
||||||
|
"[BOOT] Welcome to AeThex OS v2.1.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// If boot already played, skip straight to desktop (e.g. HOME button press)
|
||||||
|
if (hasBooted) {
|
||||||
|
Intent desktop = new Intent(this, SystemActivity.class);
|
||||||
|
desktop.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
startActivity(desktop);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Sync sound state
|
||||||
|
SoundManager.getInstance().setEnabled(themeManager.isSoundEnabled());
|
||||||
|
|
||||||
|
progressBar = findViewById(R.id.boot_progress);
|
||||||
|
percentText = findViewById(R.id.boot_percent);
|
||||||
|
statusText = findViewById(R.id.boot_status);
|
||||||
|
logText = findViewById(R.id.boot_log);
|
||||||
|
logScroll = findViewById(R.id.boot_log_scroll);
|
||||||
|
glowView = findViewById(R.id.boot_glow);
|
||||||
|
handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
// Apply theme to boot screen elements
|
||||||
|
applyBootTheme();
|
||||||
|
|
||||||
|
// Create dynamic phase UI elements
|
||||||
|
createPhaseUI();
|
||||||
|
|
||||||
|
// Animate glow pulsing
|
||||||
|
ObjectAnimator pulseAnim = ObjectAnimator.ofFloat(glowView, "alpha", 0.3f, 0.7f);
|
||||||
|
pulseAnim.setDuration(2000);
|
||||||
|
pulseAnim.setRepeatCount(ObjectAnimator.INFINITE);
|
||||||
|
pulseAnim.setRepeatMode(ObjectAnimator.REVERSE);
|
||||||
|
pulseAnim.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||||
|
pulseAnim.start();
|
||||||
|
|
||||||
|
// Fade in logo
|
||||||
|
View logoContainer = findViewById(R.id.boot_logo_container);
|
||||||
|
logoContainer.setAlpha(0f);
|
||||||
|
logoContainer.setScaleX(0.8f);
|
||||||
|
logoContainer.setScaleY(0.8f);
|
||||||
|
logoContainer.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(600).start();
|
||||||
|
|
||||||
|
// Fade in title
|
||||||
|
TextView title = findViewById(R.id.boot_title);
|
||||||
|
title.setAlpha(0f);
|
||||||
|
title.setTranslationY(20f);
|
||||||
|
title.animate().alpha(1f).translationY(0f).setStartDelay(300).setDuration(500).start();
|
||||||
|
|
||||||
|
// Start the 5-phase boot
|
||||||
|
handler.postDelayed(this::startPhase1, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBootTheme() {
|
||||||
|
int bootTextColor = themeManager.getBootTextColor(this);
|
||||||
|
Typeface displayFont = themeManager.getDisplayFont(this);
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(this);
|
||||||
|
|
||||||
|
glowView.setBackgroundResource(themeManager.getBootGlowDrawable());
|
||||||
|
|
||||||
|
FrameLayout logoContainer = findViewById(R.id.boot_logo_container);
|
||||||
|
if (logoContainer.getChildCount() > 0) {
|
||||||
|
logoContainer.getChildAt(0).setBackgroundResource(themeManager.getBootLogoDrawable());
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView bootLogo = findViewById(R.id.boot_logo);
|
||||||
|
bootLogo.setTypeface(displayFont, Typeface.BOLD);
|
||||||
|
|
||||||
|
TextView bootTitle = findViewById(R.id.boot_title);
|
||||||
|
bootTitle.setTextColor(bootTextColor);
|
||||||
|
bootTitle.setTypeface(displayFont);
|
||||||
|
|
||||||
|
TextView bootVersion = findViewById(R.id.boot_version);
|
||||||
|
bootVersion.setTypeface(monoFont);
|
||||||
|
|
||||||
|
progressBar.setProgressDrawable(
|
||||||
|
getResources().getDrawable(themeManager.getBootProgressDrawable(), getTheme()));
|
||||||
|
|
||||||
|
statusText.setTypeface(monoFont);
|
||||||
|
percentText.setTypeface(monoFont);
|
||||||
|
|
||||||
|
logText.setTextColor(bootTextColor);
|
||||||
|
logText.setTypeface(monoFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the threat level indicator and biometric scanner overlays programmatically.
|
||||||
|
*/
|
||||||
|
private void createPhaseUI() {
|
||||||
|
// Find the parent layout (the LinearLayout inside the FrameLayout root)
|
||||||
|
FrameLayout root = (FrameLayout) findViewById(R.id.boot_glow).getParent();
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(this);
|
||||||
|
|
||||||
|
// ── Threat Level indicator (bottom-left) ──
|
||||||
|
LinearLayout threatRow = new LinearLayout(this);
|
||||||
|
threatRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
threatRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
FrameLayout.LayoutParams threatParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
threatParams.gravity = Gravity.BOTTOM | Gravity.START;
|
||||||
|
threatParams.bottomMargin = dpToPx(20);
|
||||||
|
threatParams.leftMargin = dpToPx(20);
|
||||||
|
threatRow.setLayoutParams(threatParams);
|
||||||
|
threatRow.setAlpha(0f);
|
||||||
|
threatRow.setTag("threat_row");
|
||||||
|
|
||||||
|
// Pulsing dot
|
||||||
|
threatDot = new View(this);
|
||||||
|
int dotSize = dpToPx(8);
|
||||||
|
LinearLayout.LayoutParams dotParams = new LinearLayout.LayoutParams(dotSize, dotSize);
|
||||||
|
dotParams.setMarginEnd(dpToPx(8));
|
||||||
|
threatDot.setLayoutParams(dotParams);
|
||||||
|
GradientDrawable dotBg = new GradientDrawable();
|
||||||
|
dotBg.setShape(GradientDrawable.OVAL);
|
||||||
|
dotBg.setColor(Color.parseColor("#22C55E")); // starts green
|
||||||
|
threatDot.setBackground(dotBg);
|
||||||
|
threatRow.addView(threatDot);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
threatLabel = new TextView(this);
|
||||||
|
threatLabel.setText("THREAT LEVEL: SCANNING...");
|
||||||
|
threatLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
threatLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
threatLabel.setTypeface(monoFont);
|
||||||
|
threatLabel.setLetterSpacing(0.1f);
|
||||||
|
threatRow.addView(threatLabel);
|
||||||
|
|
||||||
|
root.addView(threatRow);
|
||||||
|
|
||||||
|
// ── Biometric Scanner (center overlay, hidden initially) ──
|
||||||
|
biometricContainer = new FrameLayout(this);
|
||||||
|
FrameLayout.LayoutParams bioParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
bioParams.gravity = Gravity.CENTER;
|
||||||
|
biometricContainer.setLayoutParams(bioParams);
|
||||||
|
biometricContainer.setAlpha(0f);
|
||||||
|
biometricContainer.setTag("biometric_container");
|
||||||
|
|
||||||
|
LinearLayout bioInner = new LinearLayout(this);
|
||||||
|
bioInner.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
bioInner.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
// Fingerprint icon (using text)
|
||||||
|
biometricIcon = new TextView(this);
|
||||||
|
biometricIcon.setText("⊙");
|
||||||
|
biometricIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 56);
|
||||||
|
biometricIcon.setTextColor(themeManager.getBootTextColor(this));
|
||||||
|
biometricIcon.setGravity(Gravity.CENTER);
|
||||||
|
bioInner.addView(biometricIcon);
|
||||||
|
|
||||||
|
// Status text
|
||||||
|
biometricStatus = new TextView(this);
|
||||||
|
biometricStatus.setText("BIOMETRIC SCAN");
|
||||||
|
biometricStatus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
biometricStatus.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
biometricStatus.setTypeface(monoFont);
|
||||||
|
biometricStatus.setGravity(Gravity.CENTER);
|
||||||
|
biometricStatus.setLetterSpacing(0.15f);
|
||||||
|
LinearLayout.LayoutParams bioTextParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
bioTextParams.topMargin = dpToPx(8);
|
||||||
|
biometricStatus.setLayoutParams(bioTextParams);
|
||||||
|
bioInner.addView(biometricStatus);
|
||||||
|
|
||||||
|
biometricContainer.addView(bioInner);
|
||||||
|
root.addView(biometricContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Phase 1: Hardware Scan
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void startPhase1() {
|
||||||
|
currentPhase = 1;
|
||||||
|
statusText.setText("Hardware scan...");
|
||||||
|
advancePhase1(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void advancePhase1(int index) {
|
||||||
|
if (index >= PHASE1_MSGS.length) {
|
||||||
|
startPhase2();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logText.append(PHASE1_MSGS[index] + "\n");
|
||||||
|
logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||||
|
|
||||||
|
int totalMessages = PHASE1_MSGS.length + PHASE2_MSGS.length + 4 + PHASE4_MSGS.length;
|
||||||
|
int current = index + 1;
|
||||||
|
int percent = (int) ((float) current / totalMessages * 100);
|
||||||
|
progressBar.setProgress(Math.min(percent, 100));
|
||||||
|
percentText.setText(percent + "%");
|
||||||
|
|
||||||
|
statusText.setText(getPhase1Status(index));
|
||||||
|
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.BOOT_BEEP);
|
||||||
|
|
||||||
|
int delay = 60 + (int) (Math.random() * 80);
|
||||||
|
handler.postDelayed(() -> advancePhase1(index + 1), delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPhase1Status(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0: return "Loading kernel...";
|
||||||
|
case 1: return "Checking memory...";
|
||||||
|
case 2: return "Initializing CPU...";
|
||||||
|
case 3: return "Registering handlers...";
|
||||||
|
case 4: return "Mounting filesystem...";
|
||||||
|
case 5: return "Filesystem OK";
|
||||||
|
case 6: return "GPU initialized";
|
||||||
|
case 7: return "Display configured";
|
||||||
|
default: return "Scanning...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Phase 2: Security Check + Threat Level
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void startPhase2() {
|
||||||
|
currentPhase = 2;
|
||||||
|
statusText.setText("Security check...");
|
||||||
|
|
||||||
|
// Fade in threat indicator
|
||||||
|
View threatRow = ((FrameLayout) glowView.getParent()).findViewWithTag("threat_row");
|
||||||
|
if (threatRow != null) {
|
||||||
|
threatRow.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
advancePhase2(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void advancePhase2(int index) {
|
||||||
|
if (index >= PHASE2_MSGS.length) {
|
||||||
|
// Resolve threat level
|
||||||
|
resolveThreatLevel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logText.append(PHASE2_MSGS[index] + "\n");
|
||||||
|
logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||||
|
|
||||||
|
int totalMessages = PHASE1_MSGS.length + PHASE2_MSGS.length + 4 + PHASE4_MSGS.length;
|
||||||
|
int current = PHASE1_MSGS.length + index + 1;
|
||||||
|
int percent = (int) ((float) current / totalMessages * 100);
|
||||||
|
progressBar.setProgress(Math.min(percent, 100));
|
||||||
|
percentText.setText(percent + "%");
|
||||||
|
|
||||||
|
statusText.setText("Security scanning...");
|
||||||
|
|
||||||
|
// Pulse the threat dot yellow during scan
|
||||||
|
GradientDrawable dotBg = new GradientDrawable();
|
||||||
|
dotBg.setShape(GradientDrawable.OVAL);
|
||||||
|
dotBg.setColor(Color.parseColor("#FBBF24"));
|
||||||
|
threatDot.setBackground(dotBg);
|
||||||
|
threatLabel.setText("THREAT LEVEL: SCANNING...");
|
||||||
|
threatLabel.setTextColor(Color.parseColor("#FBBF24"));
|
||||||
|
|
||||||
|
int delay = 100 + (int) (Math.random() * 100);
|
||||||
|
handler.postDelayed(() -> advancePhase2(index + 1), delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resolveThreatLevel() {
|
||||||
|
// Always resolve to LOW for now
|
||||||
|
logText.append("[SEC] Threat assessment: LOW\n");
|
||||||
|
logText.append("[SEC] All clear - no threats detected\n");
|
||||||
|
logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||||
|
|
||||||
|
GradientDrawable dotBg = new GradientDrawable();
|
||||||
|
dotBg.setShape(GradientDrawable.OVAL);
|
||||||
|
dotBg.setColor(Color.parseColor("#22C55E"));
|
||||||
|
threatDot.setBackground(dotBg);
|
||||||
|
threatLabel.setText("THREAT LEVEL: LOW");
|
||||||
|
threatLabel.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
|
||||||
|
statusText.setText("Threat: LOW");
|
||||||
|
|
||||||
|
handler.postDelayed(this::startPhase3, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Phase 3: Biometric Simulation
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void startPhase3() {
|
||||||
|
currentPhase = 3;
|
||||||
|
statusText.setText("Biometric authentication...");
|
||||||
|
|
||||||
|
logText.append("[AUTH] Initializing biometric subsystem...\n");
|
||||||
|
logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||||
|
|
||||||
|
// Show biometric scanner
|
||||||
|
biometricContainer.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
// Pulse the icon 3 times
|
||||||
|
ObjectAnimator pulse = ObjectAnimator.ofFloat(biometricIcon, "alpha", 1f, 0.3f);
|
||||||
|
pulse.setDuration(400);
|
||||||
|
pulse.setRepeatCount(5);
|
||||||
|
pulse.setRepeatMode(ObjectAnimator.REVERSE);
|
||||||
|
pulse.setInterpolator(new LinearInterpolator());
|
||||||
|
pulse.start();
|
||||||
|
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
biometricStatus.setText("SCANNING...");
|
||||||
|
biometricStatus.setTextColor(Color.parseColor("#FBBF24"));
|
||||||
|
logText.append("[AUTH] Scanning biometric data...\n");
|
||||||
|
logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||||
|
}, 600);
|
||||||
|
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
biometricStatus.setText("AUTHENTICATED ✓");
|
||||||
|
biometricStatus.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
biometricIcon.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
logText.append("[AUTH] Biometric match: CONFIRMED\n");
|
||||||
|
logText.append("[AUTH] Clearance validated: " +
|
||||||
|
(themeManager.isFoundation() ? "FOUNDATION" : "CORP") + "\n");
|
||||||
|
logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.NOTIFICATION);
|
||||||
|
}, 1400);
|
||||||
|
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
// Fade out biometric
|
||||||
|
biometricContainer.animate().alpha(0f).setDuration(200).start();
|
||||||
|
startPhase4();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Phase 4: System Init
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void startPhase4() {
|
||||||
|
currentPhase = 4;
|
||||||
|
statusText.setText("Initializing system...");
|
||||||
|
advancePhase4(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void advancePhase4(int index) {
|
||||||
|
if (index >= PHASE4_MSGS.length) {
|
||||||
|
startPhase5();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logText.append(PHASE4_MSGS[index] + "\n");
|
||||||
|
logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN));
|
||||||
|
|
||||||
|
int totalMessages = PHASE1_MSGS.length + PHASE2_MSGS.length + 4 + PHASE4_MSGS.length;
|
||||||
|
int current = PHASE1_MSGS.length + PHASE2_MSGS.length + 4 + index + 1;
|
||||||
|
int percent = (int) ((float) current / totalMessages * 100);
|
||||||
|
progressBar.setProgress(Math.min(percent, 100));
|
||||||
|
percentText.setText(percent + "%");
|
||||||
|
|
||||||
|
String[] phase4Statuses = {
|
||||||
|
"WiFi detected", "Connecting...", "Starting services...",
|
||||||
|
"Notifications ready", "Compositor ready", "Loading apps...",
|
||||||
|
"Apps found", "Loading prefs...", "Theme ready",
|
||||||
|
"Desktop ready", "Systems nominal", "Ready"
|
||||||
|
};
|
||||||
|
if (index < phase4Statuses.length) {
|
||||||
|
statusText.setText(phase4Statuses[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.BOOT_BEEP);
|
||||||
|
|
||||||
|
int delay = 70 + (int) (Math.random() * 100);
|
||||||
|
if (index == 2 || index == 5) {
|
||||||
|
delay = 250 + (int) (Math.random() * 150);
|
||||||
|
}
|
||||||
|
handler.postDelayed(() -> advancePhase4(index + 1), delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Phase 5: Transition to Desktop
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void startPhase5() {
|
||||||
|
currentPhase = 5;
|
||||||
|
progressBar.setProgress(100);
|
||||||
|
percentText.setText("100%");
|
||||||
|
statusText.setText("Launching desktop...");
|
||||||
|
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
hasBooted = true;
|
||||||
|
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
Intent intent = new Intent(MainActivity.this, SystemActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
overridePendingTransition(R.anim.scale_in, android.R.anim.fade_out);
|
||||||
|
finish();
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Lifecycle & Utility
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (handler != null) handler.removeCallbacksAndMessages(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController controller = getWindow().getInsetsController();
|
||||||
|
if (controller != null) {
|
||||||
|
controller.hide(WindowInsets.Type.systemBars());
|
||||||
|
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
720
android/app/src/main/java/com/aethex/os/MarketplaceActivity.java
Normal file
720
android/app/src/main/java/com/aethex/os/MarketplaceActivity.java
Normal file
|
|
@ -0,0 +1,720 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marketplace -- Full-featured module store with categories, search,
|
||||||
|
* install tracking, featured banner, and ALL / INSTALLED filter tabs.
|
||||||
|
*/
|
||||||
|
public class MarketplaceActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private SharedPreferences marketPrefs;
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
private boolean showInstalledOnly = false;
|
||||||
|
private String searchQuery = "";
|
||||||
|
|
||||||
|
// UI references rebuilt on every filter pass
|
||||||
|
private LinearLayout contentContainer;
|
||||||
|
private TextView tabAll;
|
||||||
|
private TextView tabInstalled;
|
||||||
|
|
||||||
|
// ── Module catalogue ──────────────────────────────────────────────
|
||||||
|
// {name, description, price, accentColor, category, installs, rating}
|
||||||
|
private static final String[][] ITEMS = {
|
||||||
|
// SECURITY
|
||||||
|
{"Cipher Module", "End-to-end encryption toolkit", "FREE", "#22C55E", "SECURITY", "12.4k", "4.9"},
|
||||||
|
{"Shield Guard", "Firewall & intrusion detection", "FREE", "#22C55E", "SECURITY", "8.7k", "4.7"},
|
||||||
|
{"Ghost Protocol", "Stealth networking & VPN tunnel", "500 Cr", "#EF4444", "SECURITY", "3.2k", "4.6"},
|
||||||
|
{"Vault Lock", "Biometric file encryption", "250 Cr", "#06B6D4", "SECURITY", "5.1k", "4.8"},
|
||||||
|
|
||||||
|
// THEMES
|
||||||
|
{"Neon Theme Pack", "3 premium animated wallpapers", "150 Cr", "#A855F7", "THEMES", "15.9k", "4.5"},
|
||||||
|
{"Retro CRT Skin", "Vintage CRT scanline overlay", "100 Cr", "#FBBF24", "THEMES", "6.3k", "4.3"},
|
||||||
|
{"Holo Glass UI", "Holographic glassmorphism theme", "200 Cr", "#06B6D4", "THEMES", "9.1k", "4.7"},
|
||||||
|
|
||||||
|
// TOOLS
|
||||||
|
{"Data Analyzer", "Real-time analytics dashboard", "300 Cr", "#06B6D4", "TOOLS", "7.8k", "4.6"},
|
||||||
|
{"Terminal Pro", "Advanced shell with autocomplete", "FREE", "#22C55E", "TOOLS", "21.0k", "4.9"},
|
||||||
|
{"Hex Editor", "Low-level binary file editor", "200 Cr", "#F97316", "TOOLS", "4.4k", "4.4"},
|
||||||
|
{"Net Scanner", "Network topology mapper", "350 Cr", "#A855F7", "TOOLS", "2.9k", "4.5"},
|
||||||
|
|
||||||
|
// ENTERTAINMENT
|
||||||
|
{"Sound Pack Pro", "12 custom notification tones", "100 Cr", "#F97316", "ENTERTAINMENT", "10.6k", "4.2"},
|
||||||
|
{"Pixel Arcade", "Retro game emulation layer", "FREE", "#22C55E", "ENTERTAINMENT", "18.3k", "4.8"},
|
||||||
|
{"Synth Studio", "Modular synthesizer workstation", "400 Cr", "#A855F7", "ENTERTAINMENT", "1.7k", "4.7"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Featured module index (Data Analyzer)
|
||||||
|
private static final int FEATURED_INDEX = 7;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
marketPrefs = getSharedPreferences("aethex_marketplace", MODE_PRIVATE);
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
TextView title = findViewById(R.id.app_title);
|
||||||
|
title.setText("Marketplace");
|
||||||
|
TextView nameDisplay = findViewById(R.id.app_name_display);
|
||||||
|
|
||||||
|
findViewById(R.id.app_back).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout content = (LinearLayout) nameDisplay.getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
content.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
|
||||||
|
content.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Wrap everything in a ScrollView
|
||||||
|
ScrollView scrollView = new ScrollView(this);
|
||||||
|
scrollView.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
scrollView.setFillViewport(true);
|
||||||
|
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
|
||||||
|
contentContainer = new LinearLayout(this);
|
||||||
|
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
contentContainer.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(24));
|
||||||
|
|
||||||
|
scrollView.addView(contentContainer);
|
||||||
|
content.addView(scrollView);
|
||||||
|
|
||||||
|
buildMarketplaceUI();
|
||||||
|
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
// Add bottom navigation bar
|
||||||
|
BottomNavBar.attach(this, (ViewGroup) root, BottomNavBar.TAB_MARKETPLACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Build the full marketplace UI into contentContainer
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void buildMarketplaceUI() {
|
||||||
|
contentContainer.removeAllViews();
|
||||||
|
|
||||||
|
Typeface displayFont = themeManager.getDisplayFont(this);
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(this);
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
|
||||||
|
// ── Search bar ───────────────────────────────────────────────
|
||||||
|
EditText searchInput = createSearchBar(monoFont);
|
||||||
|
contentContainer.addView(searchInput);
|
||||||
|
|
||||||
|
// ── Tab row: ALL / INSTALLED ─────────────────────────────────
|
||||||
|
LinearLayout tabRow = new LinearLayout(this);
|
||||||
|
tabRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
tabRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams tabRowParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
tabRowParams.topMargin = dpToPx(12);
|
||||||
|
tabRowParams.bottomMargin = dpToPx(16);
|
||||||
|
tabRow.setLayoutParams(tabRowParams);
|
||||||
|
|
||||||
|
tabAll = createTabButton("ALL", monoFont, !showInstalledOnly, primaryColor);
|
||||||
|
tabInstalled = createTabButton("INSTALLED", monoFont, showInstalledOnly, primaryColor);
|
||||||
|
|
||||||
|
tabAll.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
showInstalledOnly = false;
|
||||||
|
buildMarketplaceUI();
|
||||||
|
});
|
||||||
|
tabInstalled.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
showInstalledOnly = true;
|
||||||
|
buildMarketplaceUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
tabRow.addView(tabAll);
|
||||||
|
tabRow.addView(tabInstalled);
|
||||||
|
contentContainer.addView(tabRow);
|
||||||
|
|
||||||
|
// ── Collect visible items ────────────────────────────────────
|
||||||
|
List<String[]> visibleItems = new ArrayList<>();
|
||||||
|
for (String[] item : ITEMS) {
|
||||||
|
if (showInstalledOnly && !isInstalled(item[0])) continue;
|
||||||
|
if (!searchQuery.isEmpty() && !item[0].toLowerCase().contains(searchQuery.toLowerCase())) continue;
|
||||||
|
visibleItems.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Featured section (only when showing ALL and no search) ───
|
||||||
|
if (!showInstalledOnly && searchQuery.isEmpty()) {
|
||||||
|
buildFeaturedCard(contentContainer, displayFont, monoFont, primaryColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Group by category ────────────────────────────────────────
|
||||||
|
Map<String, List<String[]>> categorised = new LinkedHashMap<>();
|
||||||
|
for (String[] item : visibleItems) {
|
||||||
|
String cat = item[4];
|
||||||
|
categorised.computeIfAbsent(cat, k -> new ArrayList<>()).add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categorised.isEmpty()) {
|
||||||
|
TextView empty = new TextView(this);
|
||||||
|
empty.setText(showInstalledOnly ? "No installed modules yet" : "No modules found");
|
||||||
|
empty.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
empty.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
empty.setTypeface(monoFont);
|
||||||
|
empty.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams emptyParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
emptyParams.topMargin = dpToPx(40);
|
||||||
|
empty.setLayoutParams(emptyParams);
|
||||||
|
contentContainer.addView(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<String[]>> entry : categorised.entrySet()) {
|
||||||
|
String category = entry.getKey();
|
||||||
|
List<String[]> items = entry.getValue();
|
||||||
|
|
||||||
|
// Category header
|
||||||
|
addCategoryLabel(contentContainer, category, monoFont, primaryColor);
|
||||||
|
|
||||||
|
// Item cards
|
||||||
|
for (String[] item : items) {
|
||||||
|
contentContainer.addView(buildItemCard(item, displayFont, monoFont, primaryColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Search bar
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private EditText createSearchBar(Typeface monoFont) {
|
||||||
|
EditText input = new EditText(this);
|
||||||
|
input.setHint("Search modules...");
|
||||||
|
input.setHintTextColor(Color.parseColor("#55FFFFFF"));
|
||||||
|
input.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
input.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
input.setTypeface(monoFont);
|
||||||
|
input.setSingleLine(true);
|
||||||
|
input.setPadding(dpToPx(14), dpToPx(10), dpToPx(14), dpToPx(10));
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(10));
|
||||||
|
bg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
input.setBackground(bg);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
input.setLayoutParams(params);
|
||||||
|
|
||||||
|
// Suppress soft keyboard; use AeThexKeyboard instead
|
||||||
|
input.setShowSoftInputOnFocus(false);
|
||||||
|
input.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus) {
|
||||||
|
AeThexKeyboard.attachToEditText(MarketplaceActivity.this, input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.setOnClickListener(v -> AeThexKeyboard.attachToEditText(MarketplaceActivity.this, input));
|
||||||
|
|
||||||
|
// Real-time filtering
|
||||||
|
input.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
searchQuery = s.toString().trim();
|
||||||
|
// Rebuild content below search bar (keep search bar text intact)
|
||||||
|
rebuildListContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pre-fill if there was a previous query (after tab switch etc.)
|
||||||
|
if (!searchQuery.isEmpty()) {
|
||||||
|
input.setText(searchQuery);
|
||||||
|
input.setSelection(searchQuery.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild only the catalogue list content (preserving search bar state).
|
||||||
|
* Removes everything after the tab row and rebuilds categories + cards.
|
||||||
|
*/
|
||||||
|
private void rebuildListContent() {
|
||||||
|
// Remove views after index 2 (search=0, tabRow=1, rest=categories/cards)
|
||||||
|
while (contentContainer.getChildCount() > 2) {
|
||||||
|
contentContainer.removeViewAt(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Typeface displayFont = themeManager.getDisplayFont(this);
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(this);
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
|
||||||
|
List<String[]> visibleItems = new ArrayList<>();
|
||||||
|
for (String[] item : ITEMS) {
|
||||||
|
if (showInstalledOnly && !isInstalled(item[0])) continue;
|
||||||
|
if (!searchQuery.isEmpty() && !item[0].toLowerCase().contains(searchQuery.toLowerCase())) continue;
|
||||||
|
visibleItems.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Featured section only when ALL + no search
|
||||||
|
if (!showInstalledOnly && searchQuery.isEmpty()) {
|
||||||
|
buildFeaturedCard(contentContainer, displayFont, monoFont, primaryColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String[]>> categorised = new LinkedHashMap<>();
|
||||||
|
for (String[] item : visibleItems) {
|
||||||
|
String cat = item[4];
|
||||||
|
categorised.computeIfAbsent(cat, k -> new ArrayList<>()).add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categorised.isEmpty()) {
|
||||||
|
TextView empty = new TextView(this);
|
||||||
|
empty.setText(showInstalledOnly ? "No installed modules yet" : "No modules found");
|
||||||
|
empty.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
empty.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
empty.setTypeface(monoFont);
|
||||||
|
empty.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams emptyParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
emptyParams.topMargin = dpToPx(40);
|
||||||
|
empty.setLayoutParams(emptyParams);
|
||||||
|
contentContainer.addView(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<String[]>> entry : categorised.entrySet()) {
|
||||||
|
addCategoryLabel(contentContainer, entry.getKey(), monoFont, primaryColor);
|
||||||
|
for (String[] item : entry.getValue()) {
|
||||||
|
contentContainer.addView(buildItemCard(item, displayFont, monoFont, primaryColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Tab button factory
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private TextView createTabButton(String label, Typeface monoFont, boolean active, int primaryColor) {
|
||||||
|
TextView tab = new TextView(this);
|
||||||
|
tab.setText(label);
|
||||||
|
tab.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
tab.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
tab.setLetterSpacing(0.1f);
|
||||||
|
tab.setGravity(Gravity.CENTER);
|
||||||
|
tab.setPadding(dpToPx(16), dpToPx(8), dpToPx(16), dpToPx(8));
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(8));
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
int r = Color.red(primaryColor);
|
||||||
|
int g = Color.green(primaryColor);
|
||||||
|
int b = Color.blue(primaryColor);
|
||||||
|
bg.setColor(Color.argb(38, r, g, b));
|
||||||
|
bg.setStroke(dpToPx(1), primaryColor);
|
||||||
|
tab.setTextColor(primaryColor);
|
||||||
|
} else {
|
||||||
|
bg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
tab.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
}
|
||||||
|
tab.setBackground(bg);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
params.setMarginEnd(dpToPx(8));
|
||||||
|
tab.setLayoutParams(params);
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Featured card
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void buildFeaturedCard(LinearLayout parent, Typeface displayFont,
|
||||||
|
Typeface monoFont, int primaryColor) {
|
||||||
|
String[] feat = ITEMS[FEATURED_INDEX];
|
||||||
|
|
||||||
|
// Label
|
||||||
|
TextView featLabel = new TextView(this);
|
||||||
|
featLabel.setText("FEATURED");
|
||||||
|
featLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
featLabel.setTextColor(primaryColor);
|
||||||
|
featLabel.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
featLabel.setLetterSpacing(0.2f);
|
||||||
|
LinearLayout.LayoutParams flParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
flParams.bottomMargin = dpToPx(8);
|
||||||
|
featLabel.setLayoutParams(flParams);
|
||||||
|
parent.addView(featLabel);
|
||||||
|
|
||||||
|
// Card container
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
cardParams.bottomMargin = dpToPx(20);
|
||||||
|
card.setLayoutParams(cardParams);
|
||||||
|
card.setPadding(dpToPx(18), dpToPx(18), dpToPx(18), dpToPx(18));
|
||||||
|
|
||||||
|
// Gradient accent border
|
||||||
|
GradientDrawable cardBg = new GradientDrawable();
|
||||||
|
cardBg.setCornerRadius(dpToPx(14));
|
||||||
|
cardBg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
int r = Color.red(primaryColor);
|
||||||
|
int g = Color.green(primaryColor);
|
||||||
|
int b = Color.blue(primaryColor);
|
||||||
|
cardBg.setStroke(dpToPx(2), Color.argb(100, r, g, b));
|
||||||
|
card.setBackground(cardBg);
|
||||||
|
|
||||||
|
// Top row: category badge
|
||||||
|
TextView catBadge = new TextView(this);
|
||||||
|
catBadge.setText(feat[4]);
|
||||||
|
catBadge.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
catBadge.setTextColor(primaryColor);
|
||||||
|
catBadge.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
catBadge.setLetterSpacing(0.15f);
|
||||||
|
catBadge.setPadding(dpToPx(8), dpToPx(3), dpToPx(8), dpToPx(3));
|
||||||
|
GradientDrawable catBg = new GradientDrawable();
|
||||||
|
catBg.setCornerRadius(dpToPx(4));
|
||||||
|
catBg.setColor(Color.argb(25, r, g, b));
|
||||||
|
catBadge.setBackground(catBg);
|
||||||
|
LinearLayout.LayoutParams catParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
catParams.bottomMargin = dpToPx(10);
|
||||||
|
catBadge.setLayoutParams(catParams);
|
||||||
|
card.addView(catBadge);
|
||||||
|
|
||||||
|
// Name
|
||||||
|
TextView name = new TextView(this);
|
||||||
|
name.setText(feat[0]);
|
||||||
|
name.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
|
||||||
|
name.setTextColor(Color.parseColor("#EEFFFFFF"));
|
||||||
|
name.setTypeface(displayFont, Typeface.BOLD);
|
||||||
|
card.addView(name);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
TextView desc = new TextView(this);
|
||||||
|
desc.setText(feat[1]);
|
||||||
|
desc.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
desc.setTextColor(Color.parseColor("#88FFFFFF"));
|
||||||
|
desc.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams descParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
descParams.topMargin = dpToPx(4);
|
||||||
|
desc.setLayoutParams(descParams);
|
||||||
|
card.addView(desc);
|
||||||
|
|
||||||
|
// Stats row
|
||||||
|
TextView stats = new TextView(this);
|
||||||
|
stats.setText(feat[5] + " installs \u2605 " + feat[6]);
|
||||||
|
stats.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
stats.setTextColor(Color.parseColor("#55FFFFFF"));
|
||||||
|
stats.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams stParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
stParams.topMargin = dpToPx(6);
|
||||||
|
stats.setLayoutParams(stParams);
|
||||||
|
card.addView(stats);
|
||||||
|
|
||||||
|
// Price / Install button row
|
||||||
|
LinearLayout btnRow = new LinearLayout(this);
|
||||||
|
btnRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
btnRow.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams btnRowParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
btnRowParams.topMargin = dpToPx(14);
|
||||||
|
btnRow.setLayoutParams(btnRowParams);
|
||||||
|
|
||||||
|
TextView priceBtn = createPriceButton(feat, monoFont, primaryColor);
|
||||||
|
btnRow.addView(priceBtn);
|
||||||
|
card.addView(btnRow);
|
||||||
|
|
||||||
|
card.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
handleInstall(feat, priceBtn, monoFont, primaryColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.addView(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Category label
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void addCategoryLabel(LinearLayout parent, String category,
|
||||||
|
Typeface monoFont, int primaryColor) {
|
||||||
|
String colorForCategory;
|
||||||
|
switch (category) {
|
||||||
|
case "SECURITY": colorForCategory = "#EF4444"; break;
|
||||||
|
case "THEMES": colorForCategory = "#A855F7"; break;
|
||||||
|
case "TOOLS": colorForCategory = "#06B6D4"; break;
|
||||||
|
case "ENTERTAINMENT": colorForCategory = "#FBBF24"; break;
|
||||||
|
default: colorForCategory = "#66FFFFFF"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout row = new LinearLayout(this);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rowParams.topMargin = dpToPx(16);
|
||||||
|
rowParams.bottomMargin = dpToPx(8);
|
||||||
|
row.setLayoutParams(rowParams);
|
||||||
|
|
||||||
|
// Accent dot
|
||||||
|
View dot = new View(this);
|
||||||
|
int dotSize = dpToPx(6);
|
||||||
|
LinearLayout.LayoutParams dotParams = new LinearLayout.LayoutParams(dotSize, dotSize);
|
||||||
|
dotParams.setMarginEnd(dpToPx(8));
|
||||||
|
dot.setLayoutParams(dotParams);
|
||||||
|
GradientDrawable dotBg = new GradientDrawable();
|
||||||
|
dotBg.setShape(GradientDrawable.OVAL);
|
||||||
|
dotBg.setColor(Color.parseColor(colorForCategory));
|
||||||
|
dot.setBackground(dotBg);
|
||||||
|
row.addView(dot);
|
||||||
|
|
||||||
|
TextView label = new TextView(this);
|
||||||
|
label.setText(category);
|
||||||
|
label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
label.setTextColor(Color.parseColor(colorForCategory));
|
||||||
|
label.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
label.setLetterSpacing(0.15f);
|
||||||
|
row.addView(label);
|
||||||
|
|
||||||
|
// Separator line
|
||||||
|
View line = new View(this);
|
||||||
|
LinearLayout.LayoutParams lineParams = new LinearLayout.LayoutParams(
|
||||||
|
0, dpToPx(1), 1f);
|
||||||
|
lineParams.setMarginStart(dpToPx(10));
|
||||||
|
line.setLayoutParams(lineParams);
|
||||||
|
line.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
row.addView(line);
|
||||||
|
|
||||||
|
parent.addView(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Regular item card
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private LinearLayout buildItemCard(String[] item, Typeface displayFont,
|
||||||
|
Typeface monoFont, int primaryColor) {
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
card.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
cardParams.bottomMargin = dpToPx(8);
|
||||||
|
card.setLayoutParams(cardParams);
|
||||||
|
card.setPadding(dpToPx(16), dpToPx(14), dpToPx(16), dpToPx(14));
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(12));
|
||||||
|
bg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
card.setBackground(bg);
|
||||||
|
|
||||||
|
// Info column
|
||||||
|
LinearLayout info = new LinearLayout(this);
|
||||||
|
info.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
info.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
|
||||||
|
TextView name = new TextView(this);
|
||||||
|
name.setText(item[0]);
|
||||||
|
name.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
name.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
name.setTypeface(displayFont);
|
||||||
|
info.addView(name);
|
||||||
|
|
||||||
|
TextView desc = new TextView(this);
|
||||||
|
desc.setText(item[1]);
|
||||||
|
desc.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
desc.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
desc.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams dParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
dParams.topMargin = dpToPx(2);
|
||||||
|
desc.setLayoutParams(dParams);
|
||||||
|
info.addView(desc);
|
||||||
|
|
||||||
|
// Stats line: installs + rating
|
||||||
|
TextView stats = new TextView(this);
|
||||||
|
stats.setText(item[5] + " installs \u2605 " + item[6]);
|
||||||
|
stats.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
stats.setTextColor(Color.parseColor("#44FFFFFF"));
|
||||||
|
stats.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams sParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
sParams.topMargin = dpToPx(3);
|
||||||
|
stats.setLayoutParams(sParams);
|
||||||
|
info.addView(stats);
|
||||||
|
|
||||||
|
card.addView(info);
|
||||||
|
|
||||||
|
// Price / install button
|
||||||
|
TextView priceBtn = createPriceButton(item, monoFont, primaryColor);
|
||||||
|
card.addView(priceBtn);
|
||||||
|
|
||||||
|
card.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
handleInstall(item, priceBtn, monoFont, primaryColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Price button
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private TextView createPriceButton(String[] item, Typeface monoFont, int primaryColor) {
|
||||||
|
TextView price = new TextView(this);
|
||||||
|
price.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
price.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
price.setGravity(Gravity.CENTER);
|
||||||
|
price.setPadding(dpToPx(12), dpToPx(6), dpToPx(12), dpToPx(6));
|
||||||
|
|
||||||
|
boolean installed = isInstalled(item[0]);
|
||||||
|
String accentHex = item[3];
|
||||||
|
|
||||||
|
if (installed) {
|
||||||
|
price.setText("INSTALLED \u2713");
|
||||||
|
price.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
GradientDrawable pBg = new GradientDrawable();
|
||||||
|
pBg.setCornerRadius(dpToPx(6));
|
||||||
|
pBg.setColor(Color.parseColor("#1A22C55E"));
|
||||||
|
pBg.setStroke(dpToPx(1), Color.parseColor("#22C55E"));
|
||||||
|
price.setBackground(pBg);
|
||||||
|
} else {
|
||||||
|
price.setText(item[2]);
|
||||||
|
price.setTextColor(Color.parseColor(accentHex));
|
||||||
|
GradientDrawable pBg = new GradientDrawable();
|
||||||
|
pBg.setCornerRadius(dpToPx(6));
|
||||||
|
pBg.setColor(Color.parseColor("#1A000000"));
|
||||||
|
pBg.setStroke(dpToPx(1), Color.parseColor(accentHex));
|
||||||
|
price.setBackground(pBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Install logic
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void handleInstall(String[] item, TextView priceBtn, Typeface monoFont, int primaryColor) {
|
||||||
|
if (isInstalled(item[0])) {
|
||||||
|
AeThexToast.show(this, item[0] + " is already installed", AeThexToast.Type.INFO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set to "Installing..." state
|
||||||
|
priceBtn.setText("Installing...");
|
||||||
|
priceBtn.setTextColor(Color.parseColor("#FBBF24"));
|
||||||
|
GradientDrawable loadingBg = new GradientDrawable();
|
||||||
|
loadingBg.setCornerRadius(dpToPx(6));
|
||||||
|
loadingBg.setColor(Color.parseColor("#1AFBBF24"));
|
||||||
|
loadingBg.setStroke(dpToPx(1), Color.parseColor("#FBBF24"));
|
||||||
|
priceBtn.setBackground(loadingBg);
|
||||||
|
|
||||||
|
// After 500ms, mark as installed
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
setInstalled(item[0], true);
|
||||||
|
|
||||||
|
priceBtn.setText("INSTALLED \u2713");
|
||||||
|
priceBtn.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
GradientDrawable doneBg = new GradientDrawable();
|
||||||
|
doneBg.setCornerRadius(dpToPx(6));
|
||||||
|
doneBg.setColor(Color.parseColor("#1A22C55E"));
|
||||||
|
doneBg.setStroke(dpToPx(1), Color.parseColor("#22C55E"));
|
||||||
|
priceBtn.setBackground(doneBg);
|
||||||
|
|
||||||
|
AeThexToast.show(this, item[0] + " installed successfully", AeThexToast.Type.SUCCESS);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// SharedPreferences helpers
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private boolean isInstalled(String moduleName) {
|
||||||
|
return marketPrefs.getBoolean("installed_" + moduleName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInstalled(String moduleName, boolean installed) {
|
||||||
|
marketPrefs.edit().putBoolean("installed_" + moduleName, installed).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Utility
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
362
android/app/src/main/java/com/aethex/os/MinesweeperActivity.java
Normal file
362
android/app/src/main/java/com/aethex/os/MinesweeperActivity.java
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class MinesweeperActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
|
||||||
|
private static final int ROWS = 9;
|
||||||
|
private static final int COLS = 9;
|
||||||
|
private static final int MINE_COUNT = 10;
|
||||||
|
|
||||||
|
private int[][] board; // -1 = mine, 0-8 = adjacent mines
|
||||||
|
private boolean[][] revealed;
|
||||||
|
private boolean[][] flagged;
|
||||||
|
private boolean gameOver;
|
||||||
|
private boolean gameWon;
|
||||||
|
private boolean flagMode;
|
||||||
|
private int minesRemaining;
|
||||||
|
private int secondsElapsed;
|
||||||
|
private boolean timerRunning;
|
||||||
|
|
||||||
|
private MineFieldView mineFieldView;
|
||||||
|
private TextView mineCountText;
|
||||||
|
private TextView timerText;
|
||||||
|
private TextView flagToggle;
|
||||||
|
private Handler timerHandler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_minesweeper);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
mineCountText = findViewById(R.id.mine_count);
|
||||||
|
timerText = findViewById(R.id.mine_timer);
|
||||||
|
flagToggle = findViewById(R.id.mine_flag_toggle);
|
||||||
|
timerHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
findViewById(R.id.mine_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.mine_new_game).setOnClickListener(v -> {
|
||||||
|
v.animate().scaleX(0.9f).scaleY(0.9f).setDuration(60).withEndAction(() -> {
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(60).start();
|
||||||
|
startNewGame();
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
flagToggle.setOnClickListener(v -> {
|
||||||
|
flagMode = !flagMode;
|
||||||
|
flagToggle.setText(flagMode ? "MODE: FLAG" : "MODE: REVEAL");
|
||||||
|
flagToggle.setTextColor(flagMode ? 0xFFF87171 : 0xFFFFFFFF);
|
||||||
|
});
|
||||||
|
|
||||||
|
FrameLayout container = findViewById(R.id.mine_board_container);
|
||||||
|
mineFieldView = new MineFieldView(this);
|
||||||
|
container.addView(mineFieldView);
|
||||||
|
|
||||||
|
startNewGame();
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View root = findViewById(R.id.minesweeper_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startNewGame() {
|
||||||
|
board = new int[ROWS][COLS];
|
||||||
|
revealed = new boolean[ROWS][COLS];
|
||||||
|
flagged = new boolean[ROWS][COLS];
|
||||||
|
gameOver = false;
|
||||||
|
gameWon = false;
|
||||||
|
flagMode = false;
|
||||||
|
minesRemaining = MINE_COUNT;
|
||||||
|
secondsElapsed = 0;
|
||||||
|
timerRunning = false;
|
||||||
|
|
||||||
|
flagToggle.setText("MODE: REVEAL");
|
||||||
|
flagToggle.setTextColor(0xFFFFFFFF);
|
||||||
|
updateMineCount();
|
||||||
|
timerText.setText("0:00");
|
||||||
|
timerHandler.removeCallbacksAndMessages(null);
|
||||||
|
|
||||||
|
// Place mines
|
||||||
|
Random rand = new Random();
|
||||||
|
int placed = 0;
|
||||||
|
while (placed < MINE_COUNT) {
|
||||||
|
int r = rand.nextInt(ROWS);
|
||||||
|
int c = rand.nextInt(COLS);
|
||||||
|
if (board[r][c] != -1) {
|
||||||
|
board[r][c] = -1;
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate numbers
|
||||||
|
for (int r = 0; r < ROWS; r++) {
|
||||||
|
for (int c = 0; c < COLS; c++) {
|
||||||
|
if (board[r][c] == -1) continue;
|
||||||
|
int count = 0;
|
||||||
|
for (int dr = -1; dr <= 1; dr++) {
|
||||||
|
for (int dc = -1; dc <= 1; dc++) {
|
||||||
|
int nr = r + dr, nc = c + dc;
|
||||||
|
if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && board[nr][nc] == -1) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
board[r][c] = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mineFieldView != null) mineFieldView.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startTimer() {
|
||||||
|
if (timerRunning) return;
|
||||||
|
timerRunning = true;
|
||||||
|
timerHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (!timerRunning || gameOver || gameWon) return;
|
||||||
|
secondsElapsed++;
|
||||||
|
int mins = secondsElapsed / 60;
|
||||||
|
int secs = secondsElapsed % 60;
|
||||||
|
timerText.setText(String.format("%d:%02d", mins, secs));
|
||||||
|
timerHandler.postDelayed(this, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCellClick(int row, int col) {
|
||||||
|
if (gameOver || gameWon) return;
|
||||||
|
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return;
|
||||||
|
|
||||||
|
if (!timerRunning) startTimer();
|
||||||
|
|
||||||
|
if (flagMode) {
|
||||||
|
if (!revealed[row][col]) {
|
||||||
|
flagged[row][col] = !flagged[row][col];
|
||||||
|
minesRemaining += flagged[row][col] ? -1 : 1;
|
||||||
|
updateMineCount();
|
||||||
|
mineFieldView.invalidate();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flagged[row][col] || revealed[row][col]) return;
|
||||||
|
|
||||||
|
if (board[row][col] == -1) {
|
||||||
|
// Hit a mine - game over
|
||||||
|
gameOver = true;
|
||||||
|
timerRunning = false;
|
||||||
|
revealAll();
|
||||||
|
mineFieldView.invalidate();
|
||||||
|
Toast.makeText(this, "Game Over! Tap NEW to restart.", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reveal(row, col);
|
||||||
|
checkWin();
|
||||||
|
mineFieldView.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reveal(int row, int col) {
|
||||||
|
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return;
|
||||||
|
if (revealed[row][col] || flagged[row][col]) return;
|
||||||
|
|
||||||
|
revealed[row][col] = true;
|
||||||
|
|
||||||
|
if (board[row][col] == 0) {
|
||||||
|
for (int dr = -1; dr <= 1; dr++) {
|
||||||
|
for (int dc = -1; dc <= 1; dc++) {
|
||||||
|
reveal(row + dr, col + dc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void revealAll() {
|
||||||
|
for (int r = 0; r < ROWS; r++) {
|
||||||
|
for (int c = 0; c < COLS; c++) {
|
||||||
|
revealed[r][c] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkWin() {
|
||||||
|
int unrevealed = 0;
|
||||||
|
for (int r = 0; r < ROWS; r++) {
|
||||||
|
for (int c = 0; c < COLS; c++) {
|
||||||
|
if (!revealed[r][c]) unrevealed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unrevealed == MINE_COUNT) {
|
||||||
|
gameWon = true;
|
||||||
|
timerRunning = false;
|
||||||
|
Toast.makeText(this, "You Win! Time: " + timerText.getText(), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMineCount() {
|
||||||
|
mineCountText.setText("Mines: " + minesRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MineFieldView extends View {
|
||||||
|
private Paint cellPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private Paint gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private RectF cellRect = new RectF();
|
||||||
|
|
||||||
|
private int[] NUMBER_COLORS;
|
||||||
|
|
||||||
|
public MineFieldView(Context context) {
|
||||||
|
super(context);
|
||||||
|
int primary = themeManager.getPrimaryColor(context);
|
||||||
|
NUMBER_COLORS = new int[] {
|
||||||
|
0x00000000, // 0 - unused
|
||||||
|
primary, // 1 - themed primary
|
||||||
|
0xFF34D399, // 2 - green
|
||||||
|
0xFFF87171, // 3 - red
|
||||||
|
0xFFA855F7, // 4 - purple
|
||||||
|
0xFFFBBF24, // 5 - yellow
|
||||||
|
primary, // 6 - themed primary
|
||||||
|
0xFFFFFFFF, // 7 - white
|
||||||
|
0xFF94A3B8, // 8 - gray
|
||||||
|
};
|
||||||
|
gridPaint.setColor(0x1AFFFFFF);
|
||||||
|
gridPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
gridPaint.setStrokeWidth(1f);
|
||||||
|
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
textPaint.setTypeface(android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.BOLD));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
if (board == null) return;
|
||||||
|
|
||||||
|
float w = getWidth();
|
||||||
|
float h = getHeight();
|
||||||
|
float cellSize = Math.min(w / COLS, h / ROWS);
|
||||||
|
float offsetX = (w - cellSize * COLS) / 2;
|
||||||
|
float offsetY = (h - cellSize * ROWS) / 2;
|
||||||
|
float radius = cellSize * 0.15f;
|
||||||
|
textPaint.setTextSize(cellSize * 0.45f);
|
||||||
|
|
||||||
|
for (int r = 0; r < ROWS; r++) {
|
||||||
|
for (int c = 0; c < COLS; c++) {
|
||||||
|
float x = offsetX + c * cellSize;
|
||||||
|
float y = offsetY + r * cellSize;
|
||||||
|
cellRect.set(x + 2, y + 2, x + cellSize - 2, y + cellSize - 2);
|
||||||
|
|
||||||
|
if (revealed[r][c]) {
|
||||||
|
if (board[r][c] == -1) {
|
||||||
|
// Mine
|
||||||
|
cellPaint.setColor(gameOver ? 0x40F87171 : 0x1AFFFFFF);
|
||||||
|
canvas.drawRoundRect(cellRect, radius, radius, cellPaint);
|
||||||
|
textPaint.setColor(0xFFF87171);
|
||||||
|
canvas.drawText("\u25CF", cellRect.centerX(),
|
||||||
|
cellRect.centerY() + textPaint.getTextSize() / 3, textPaint);
|
||||||
|
} else if (board[r][c] > 0) {
|
||||||
|
// Number
|
||||||
|
cellPaint.setColor(0x0DFFFFFF);
|
||||||
|
canvas.drawRoundRect(cellRect, radius, radius, cellPaint);
|
||||||
|
textPaint.setColor(NUMBER_COLORS[board[r][c]]);
|
||||||
|
canvas.drawText(String.valueOf(board[r][c]), cellRect.centerX(),
|
||||||
|
cellRect.centerY() + textPaint.getTextSize() / 3, textPaint);
|
||||||
|
} else {
|
||||||
|
// Empty revealed
|
||||||
|
cellPaint.setColor(0x08FFFFFF);
|
||||||
|
canvas.drawRoundRect(cellRect, radius, radius, cellPaint);
|
||||||
|
}
|
||||||
|
} else if (flagged[r][c]) {
|
||||||
|
// Flagged
|
||||||
|
cellPaint.setColor(0x26F87171);
|
||||||
|
canvas.drawRoundRect(cellRect, radius, radius, cellPaint);
|
||||||
|
textPaint.setColor(0xFFF87171);
|
||||||
|
canvas.drawText("\u2691", cellRect.centerX(),
|
||||||
|
cellRect.centerY() + textPaint.getTextSize() / 3, textPaint);
|
||||||
|
} else {
|
||||||
|
// Unrevealed
|
||||||
|
cellPaint.setColor(0x1AFFFFFF);
|
||||||
|
canvas.drawRoundRect(cellRect, radius, radius, cellPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid outline
|
||||||
|
canvas.drawRoundRect(cellRect, radius, radius, gridPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||||
|
float w = getWidth();
|
||||||
|
float h = getHeight();
|
||||||
|
float cellSize = Math.min(w / COLS, h / ROWS);
|
||||||
|
float offsetX = (w - cellSize * COLS) / 2;
|
||||||
|
float offsetY = (h - cellSize * ROWS) / 2;
|
||||||
|
|
||||||
|
int col = (int) ((event.getX() - offsetX) / cellSize);
|
||||||
|
int row = (int) ((event.getY() - offsetY) / cellSize);
|
||||||
|
|
||||||
|
handleCellClick(row, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
timerRunning = false;
|
||||||
|
timerHandler.removeCallbacksAndMessages(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
android/app/src/main/java/com/aethex/os/ModuleManager.java
Normal file
156
android/app/src/main/java/com/aethex/os/ModuleManager.java
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModuleManager - Tracks installed marketplace modules and provides their functionality.
|
||||||
|
* Modules actually affect system behavior when installed.
|
||||||
|
*/
|
||||||
|
public class ModuleManager {
|
||||||
|
|
||||||
|
private static ModuleManager instance;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
|
// Module IDs (must match marketplace item names exactly)
|
||||||
|
public static final String CIPHER_MODULE = "Cipher Module";
|
||||||
|
public static final String SHIELD_GUARD = "Shield Guard";
|
||||||
|
public static final String GHOST_PROTOCOL = "Ghost Protocol";
|
||||||
|
public static final String VAULT_LOCK = "Vault Lock";
|
||||||
|
public static final String NEON_THEME_PACK = "Neon Theme Pack";
|
||||||
|
public static final String RETRO_CRT_SKIN = "Retro CRT Skin";
|
||||||
|
public static final String HOLO_GLASS_UI = "Holo Glass UI";
|
||||||
|
public static final String DATA_ANALYZER = "Data Analyzer";
|
||||||
|
public static final String TERMINAL_PRO = "Terminal Pro";
|
||||||
|
public static final String HEX_EDITOR = "Hex Editor";
|
||||||
|
public static final String NET_SCANNER = "Net Scanner";
|
||||||
|
public static final String SOUND_PACK_PRO = "Sound Pack Pro";
|
||||||
|
public static final String PIXEL_ARCADE = "Pixel Arcade";
|
||||||
|
public static final String SYNTH_STUDIO = "Synth Studio";
|
||||||
|
|
||||||
|
private ModuleManager(Context context) {
|
||||||
|
prefs = context.getSharedPreferences("aethex_marketplace", Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModuleManager getInstance(Context context) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new ModuleManager(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInstalled(String moduleName) {
|
||||||
|
return prefs.getBoolean("installed_" + moduleName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInstalled(String moduleName, boolean installed) {
|
||||||
|
prefs.edit().putBoolean("installed_" + moduleName, installed).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Module feature checks
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** Cipher Module - Enables encrypted notes in Projects */
|
||||||
|
public boolean hasEncryption() {
|
||||||
|
return isInstalled(CIPHER_MODULE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shield Guard - Enables app lock / privacy features */
|
||||||
|
public boolean hasAppLock() {
|
||||||
|
return isInstalled(SHIELD_GUARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ghost Protocol - Enables stealth mode (hide recent apps, etc.) */
|
||||||
|
public boolean hasStealthMode() {
|
||||||
|
return isInstalled(GHOST_PROTOCOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vault Lock - Enables biometric file lock */
|
||||||
|
public boolean hasBiometricLock() {
|
||||||
|
return isInstalled(VAULT_LOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Neon Theme Pack - Unlocks neon wallpapers */
|
||||||
|
public boolean hasNeonThemes() {
|
||||||
|
return isInstalled(NEON_THEME_PACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retro CRT Skin - Enables CRT scanline overlay */
|
||||||
|
public boolean hasCRTOverlay() {
|
||||||
|
return isInstalled(RETRO_CRT_SKIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Holo Glass UI - Enables extra glassmorphism effects */
|
||||||
|
public boolean hasHoloGlass() {
|
||||||
|
return isInstalled(HOLO_GLASS_UI);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Data Analyzer - Enables system stats widget */
|
||||||
|
public boolean hasDataAnalyzer() {
|
||||||
|
return isInstalled(DATA_ANALYZER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Terminal Pro - Enables advanced terminal features */
|
||||||
|
public boolean hasTerminalPro() {
|
||||||
|
return isInstalled(TERMINAL_PRO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hex Editor - Enables hex view in file browser */
|
||||||
|
public boolean hasHexEditor() {
|
||||||
|
return isInstalled(HEX_EDITOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Net Scanner - Enables network scanning tools */
|
||||||
|
public boolean hasNetScanner() {
|
||||||
|
return isInstalled(NET_SCANNER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sound Pack Pro - Unlocks extra sound effects */
|
||||||
|
public boolean hasSoundPackPro() {
|
||||||
|
return isInstalled(SOUND_PACK_PRO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pixel Arcade - Enables games section */
|
||||||
|
public boolean hasPixelArcade() {
|
||||||
|
return isInstalled(PIXEL_ARCADE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Synth Studio - Enables synth app */
|
||||||
|
public boolean hasSynthStudio() {
|
||||||
|
return isInstalled(SYNTH_STUDIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Module count helpers
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
public int getInstalledCount() {
|
||||||
|
int count = 0;
|
||||||
|
String[] modules = {
|
||||||
|
CIPHER_MODULE, SHIELD_GUARD, GHOST_PROTOCOL, VAULT_LOCK,
|
||||||
|
NEON_THEME_PACK, RETRO_CRT_SKIN, HOLO_GLASS_UI,
|
||||||
|
DATA_ANALYZER, TERMINAL_PRO, HEX_EDITOR, NET_SCANNER,
|
||||||
|
SOUND_PACK_PRO, PIXEL_ARCADE, SYNTH_STUDIO
|
||||||
|
};
|
||||||
|
for (String mod : modules) {
|
||||||
|
if (isInstalled(mod)) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalCreditsSpent() {
|
||||||
|
int total = 0;
|
||||||
|
if (isInstalled(GHOST_PROTOCOL)) total += 500;
|
||||||
|
if (isInstalled(VAULT_LOCK)) total += 250;
|
||||||
|
if (isInstalled(NEON_THEME_PACK)) total += 150;
|
||||||
|
if (isInstalled(RETRO_CRT_SKIN)) total += 100;
|
||||||
|
if (isInstalled(HOLO_GLASS_UI)) total += 200;
|
||||||
|
if (isInstalled(DATA_ANALYZER)) total += 300;
|
||||||
|
if (isInstalled(HEX_EDITOR)) total += 200;
|
||||||
|
if (isInstalled(NET_SCANNER)) total += 350;
|
||||||
|
if (isInstalled(SOUND_PACK_PRO)) total += 100;
|
||||||
|
if (isInstalled(SYNTH_STUDIO)) total += 400;
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
452
android/app/src/main/java/com/aethex/os/MusicActivity.java
Normal file
452
android/app/src/main/java/com/aethex/os/MusicActivity.java
Normal file
|
|
@ -0,0 +1,452 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
public class MusicActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
|
||||||
|
// Track data arrays
|
||||||
|
private final String[] trackTitles = {
|
||||||
|
"Neon Horizons", "Midnight Protocol", "Binary Sunset",
|
||||||
|
"Electric Dreams", "Data Stream"
|
||||||
|
};
|
||||||
|
private final String[] trackArtists = {
|
||||||
|
"Digital Pulse", "CyberWave", "The Algorithms",
|
||||||
|
"Neural Network", "Quantum Loop"
|
||||||
|
};
|
||||||
|
private final String[] trackAlbums = {
|
||||||
|
"Synthwave Collection", "Dark Net Sessions", "Algorithmic Beats",
|
||||||
|
"Neural Pathways", "Quantum Frequencies"
|
||||||
|
};
|
||||||
|
// Durations in seconds
|
||||||
|
private final int[] trackDurations = { 225, 252, 178, 323, 211 };
|
||||||
|
|
||||||
|
private int currentTrack = 0;
|
||||||
|
private boolean isPlaying = false;
|
||||||
|
private boolean shuffleOn = false;
|
||||||
|
private boolean repeatOn = false;
|
||||||
|
private int elapsedMillis = 0;
|
||||||
|
|
||||||
|
private TextView trackTitle;
|
||||||
|
private TextView trackArtist;
|
||||||
|
private TextView trackAlbum;
|
||||||
|
private TextView timeElapsed;
|
||||||
|
private TextView timeTotal;
|
||||||
|
private TextView playPauseBtn;
|
||||||
|
private TextView shuffleBtn;
|
||||||
|
private TextView repeatBtn;
|
||||||
|
private SeekBar seekBar;
|
||||||
|
private View albumArt;
|
||||||
|
private LinearLayout upNextContainer;
|
||||||
|
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
private final Runnable playbackRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (!isPlaying) return;
|
||||||
|
|
||||||
|
elapsedMillis += 100;
|
||||||
|
int totalMillis = trackDurations[currentTrack] * 1000;
|
||||||
|
|
||||||
|
if (elapsedMillis >= totalMillis) {
|
||||||
|
// Track finished
|
||||||
|
if (repeatOn) {
|
||||||
|
elapsedMillis = 0;
|
||||||
|
} else {
|
||||||
|
nextTrack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSeekBar();
|
||||||
|
updateTimeLabels();
|
||||||
|
handler.postDelayed(this, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_music);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Find views
|
||||||
|
trackTitle = findViewById(R.id.music_track_title);
|
||||||
|
trackArtist = findViewById(R.id.music_track_artist);
|
||||||
|
trackAlbum = findViewById(R.id.music_track_album);
|
||||||
|
timeElapsed = findViewById(R.id.music_time_elapsed);
|
||||||
|
timeTotal = findViewById(R.id.music_time_total);
|
||||||
|
playPauseBtn = findViewById(R.id.music_play_pause);
|
||||||
|
shuffleBtn = findViewById(R.id.music_shuffle);
|
||||||
|
repeatBtn = findViewById(R.id.music_repeat);
|
||||||
|
seekBar = findViewById(R.id.music_seekbar);
|
||||||
|
albumArt = findViewById(R.id.music_album_art);
|
||||||
|
upNextContainer = findViewById(R.id.music_up_next_container);
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
findViewById(R.id.music_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup album art gradient
|
||||||
|
setupAlbumArt();
|
||||||
|
|
||||||
|
// Setup play/pause button background (larger circle)
|
||||||
|
setupPlayPauseBackground();
|
||||||
|
|
||||||
|
// Display initial track
|
||||||
|
displayTrack(currentTrack);
|
||||||
|
|
||||||
|
// Build up-next list
|
||||||
|
buildUpNextList();
|
||||||
|
|
||||||
|
// --- Transport controls ---
|
||||||
|
|
||||||
|
playPauseBtn.setOnClickListener(v -> {
|
||||||
|
v.animate().scaleX(0.9f).scaleY(0.9f).setDuration(60).withEndAction(() ->
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(60).start()
|
||||||
|
).start();
|
||||||
|
togglePlayPause();
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.music_next).setOnClickListener(v -> {
|
||||||
|
v.animate().scaleX(0.85f).scaleY(0.85f).setDuration(60).withEndAction(() ->
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(60).start()
|
||||||
|
).start();
|
||||||
|
nextTrack();
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.music_prev).setOnClickListener(v -> {
|
||||||
|
v.animate().scaleX(0.85f).scaleY(0.85f).setDuration(60).withEndAction(() ->
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(60).start()
|
||||||
|
).start();
|
||||||
|
prevTrack();
|
||||||
|
});
|
||||||
|
|
||||||
|
shuffleBtn.setOnClickListener(v -> {
|
||||||
|
shuffleOn = !shuffleOn;
|
||||||
|
shuffleBtn.setTextColor(shuffleOn ? themeManager.getPrimaryColor(MusicActivity.this) : 0x66FFFFFF);
|
||||||
|
});
|
||||||
|
|
||||||
|
repeatBtn.setOnClickListener(v -> {
|
||||||
|
repeatOn = !repeatOn;
|
||||||
|
repeatBtn.setTextColor(repeatOn ? themeManager.getPrimaryColor(MusicActivity.this) : 0x66FFFFFF);
|
||||||
|
});
|
||||||
|
|
||||||
|
// SeekBar listener
|
||||||
|
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) {
|
||||||
|
if (fromUser) {
|
||||||
|
int totalMillis = trackDurations[currentTrack] * 1000;
|
||||||
|
elapsedMillis = (int) ((progress / 1000f) * totalMillis);
|
||||||
|
updateTimeLabels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar sb) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar sb) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View root = findViewById(R.id.music_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupAlbumArt() {
|
||||||
|
int primary = themeManager.getPrimaryColor(this);
|
||||||
|
GradientDrawable gradient = new GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.TL_BR,
|
||||||
|
new int[] { primary, 0xFF7C3AED, 0xFFA855F7 }
|
||||||
|
);
|
||||||
|
gradient.setCornerRadius(dpToPx(16));
|
||||||
|
albumArt.setBackground(gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupPlayPauseBackground() {
|
||||||
|
GradientDrawable circle = new GradientDrawable();
|
||||||
|
circle.setShape(GradientDrawable.OVAL);
|
||||||
|
circle.setColor(0x33FFFFFF);
|
||||||
|
circle.setStroke(dpToPx(1), 0x4DFFFFFF);
|
||||||
|
playPauseBtn.setBackground(circle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayTrack(int index) {
|
||||||
|
trackTitle.setText(trackTitles[index]);
|
||||||
|
trackArtist.setText(trackArtists[index]);
|
||||||
|
trackAlbum.setText(trackAlbums[index]);
|
||||||
|
timeTotal.setText(formatTime(trackDurations[index]));
|
||||||
|
timeElapsed.setText("0:00");
|
||||||
|
seekBar.setProgress(0);
|
||||||
|
elapsedMillis = 0;
|
||||||
|
|
||||||
|
// Animate the album art with a subtle color shift per track
|
||||||
|
updateAlbumArtForTrack(index);
|
||||||
|
|
||||||
|
// Animate track title in
|
||||||
|
trackTitle.setAlpha(0f);
|
||||||
|
trackTitle.setTranslationY(10f);
|
||||||
|
trackTitle.animate().alpha(1f).translationY(0f).setDuration(200).start();
|
||||||
|
|
||||||
|
trackArtist.setAlpha(0f);
|
||||||
|
trackArtist.animate().alpha(1f).setStartDelay(50).setDuration(200).start();
|
||||||
|
|
||||||
|
trackAlbum.setAlpha(0f);
|
||||||
|
trackAlbum.animate().alpha(1f).setStartDelay(100).setDuration(200).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAlbumArtForTrack(int index) {
|
||||||
|
// Each track gets a slightly different gradient
|
||||||
|
int primary = themeManager.getPrimaryColor(this);
|
||||||
|
int[][] gradients = {
|
||||||
|
{ primary, 0xFF7C3AED, 0xFFA855F7 }, // Primary to purple
|
||||||
|
{ 0xFF1E40AF, 0xFF6D28D9, 0xFFDB2777 }, // Blue to pink
|
||||||
|
{ 0xFF059669, 0xFF0891B2, 0xFF6366F1 }, // Green to indigo
|
||||||
|
{ 0xFFF59E0B, 0xFFEF4444, 0xFF8B5CF6 }, // Amber to purple
|
||||||
|
{ primary, 0xFF3B82F6, 0xFF8B5CF6 }, // Primary to blue to violet
|
||||||
|
};
|
||||||
|
|
||||||
|
GradientDrawable gradient = new GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.TL_BR,
|
||||||
|
gradients[index % gradients.length]
|
||||||
|
);
|
||||||
|
gradient.setCornerRadius(dpToPx(16));
|
||||||
|
albumArt.setBackground(gradient);
|
||||||
|
|
||||||
|
// Subtle scale animation
|
||||||
|
albumArt.setScaleX(0.95f);
|
||||||
|
albumArt.setScaleY(0.95f);
|
||||||
|
albumArt.animate().scaleX(1f).scaleY(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildUpNextList() {
|
||||||
|
upNextContainer.removeAllViews();
|
||||||
|
|
||||||
|
// Show up to 4 tracks after the current one
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < trackTitles.length && count < 4; i++) {
|
||||||
|
if (i == currentTrack) continue;
|
||||||
|
|
||||||
|
final int trackIndex = i;
|
||||||
|
|
||||||
|
LinearLayout row = new LinearLayout(this);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
row.setPadding(dpToPx(12), dpToPx(10), dpToPx(12), dpToPx(10));
|
||||||
|
|
||||||
|
// Track number indicator
|
||||||
|
TextView numView = new TextView(this);
|
||||||
|
numView.setText(String.valueOf(i + 1));
|
||||||
|
numView.setTextSize(12);
|
||||||
|
numView.setTextColor(0x66FFFFFF);
|
||||||
|
numView.setTypeface(themeManager.getMonoFont(MusicActivity.this));
|
||||||
|
numView.setWidth(dpToPx(24));
|
||||||
|
row.addView(numView);
|
||||||
|
|
||||||
|
// Track info column
|
||||||
|
LinearLayout infoCol = new LinearLayout(this);
|
||||||
|
infoCol.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
LinearLayout.LayoutParams infoParams = new LinearLayout.LayoutParams(
|
||||||
|
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
infoCol.setLayoutParams(infoParams);
|
||||||
|
|
||||||
|
TextView titleView = new TextView(this);
|
||||||
|
titleView.setText(trackTitles[i]);
|
||||||
|
titleView.setTextSize(13);
|
||||||
|
titleView.setTextColor(0xCCFFFFFF);
|
||||||
|
titleView.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
|
||||||
|
titleView.setSingleLine(true);
|
||||||
|
infoCol.addView(titleView);
|
||||||
|
|
||||||
|
TextView artistView = new TextView(this);
|
||||||
|
artistView.setText(trackArtists[i]);
|
||||||
|
artistView.setTextSize(11);
|
||||||
|
artistView.setTextColor(0x99FFFFFF);
|
||||||
|
artistView.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
|
||||||
|
artistView.setSingleLine(true);
|
||||||
|
infoCol.addView(artistView);
|
||||||
|
|
||||||
|
row.addView(infoCol);
|
||||||
|
|
||||||
|
// Duration
|
||||||
|
TextView durView = new TextView(this);
|
||||||
|
durView.setText(formatTime(trackDurations[i]));
|
||||||
|
durView.setTextSize(11);
|
||||||
|
durView.setTextColor(0x66FFFFFF);
|
||||||
|
durView.setTypeface(themeManager.getMonoFont(MusicActivity.this));
|
||||||
|
row.addView(durView);
|
||||||
|
|
||||||
|
// Tap to jump to track
|
||||||
|
row.setOnClickListener(v -> {
|
||||||
|
boolean wasPlaying = isPlaying;
|
||||||
|
if (isPlaying) {
|
||||||
|
isPlaying = false;
|
||||||
|
handler.removeCallbacks(playbackRunnable);
|
||||||
|
}
|
||||||
|
currentTrack = trackIndex;
|
||||||
|
displayTrack(currentTrack);
|
||||||
|
buildUpNextList();
|
||||||
|
if (wasPlaying) {
|
||||||
|
togglePlayPause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
upNextContainer.addView(row);
|
||||||
|
|
||||||
|
// Add divider between items (not after last)
|
||||||
|
count++;
|
||||||
|
if (count < 4 && (i + 1 < trackTitles.length || count < 3)) {
|
||||||
|
View divider = new View(this);
|
||||||
|
divider.setBackgroundColor(0x1AFFFFFF);
|
||||||
|
LinearLayout.LayoutParams divParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, dpToPx(1));
|
||||||
|
divParams.setMarginStart(dpToPx(36));
|
||||||
|
divParams.setMarginEnd(dpToPx(12));
|
||||||
|
divider.setLayoutParams(divParams);
|
||||||
|
upNextContainer.addView(divider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void togglePlayPause() {
|
||||||
|
isPlaying = !isPlaying;
|
||||||
|
if (isPlaying) {
|
||||||
|
playPauseBtn.setText("\u23F8"); // Pause symbol
|
||||||
|
handler.post(playbackRunnable);
|
||||||
|
} else {
|
||||||
|
playPauseBtn.setText("\u25B6"); // Play symbol
|
||||||
|
handler.removeCallbacks(playbackRunnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nextTrack() {
|
||||||
|
boolean wasPlaying = isPlaying;
|
||||||
|
isPlaying = false;
|
||||||
|
handler.removeCallbacks(playbackRunnable);
|
||||||
|
|
||||||
|
if (shuffleOn) {
|
||||||
|
int next;
|
||||||
|
do {
|
||||||
|
next = (int) (Math.random() * trackTitles.length);
|
||||||
|
} while (next == currentTrack && trackTitles.length > 1);
|
||||||
|
currentTrack = next;
|
||||||
|
} else {
|
||||||
|
currentTrack = (currentTrack + 1) % trackTitles.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayTrack(currentTrack);
|
||||||
|
buildUpNextList();
|
||||||
|
|
||||||
|
if (wasPlaying) {
|
||||||
|
togglePlayPause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prevTrack() {
|
||||||
|
boolean wasPlaying = isPlaying;
|
||||||
|
isPlaying = false;
|
||||||
|
handler.removeCallbacks(playbackRunnable);
|
||||||
|
|
||||||
|
// If more than 3 seconds in, restart current track; else go to previous
|
||||||
|
if (elapsedMillis > 3000) {
|
||||||
|
elapsedMillis = 0;
|
||||||
|
updateSeekBar();
|
||||||
|
updateTimeLabels();
|
||||||
|
if (wasPlaying) {
|
||||||
|
togglePlayPause();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shuffleOn) {
|
||||||
|
int prev;
|
||||||
|
do {
|
||||||
|
prev = (int) (Math.random() * trackTitles.length);
|
||||||
|
} while (prev == currentTrack && trackTitles.length > 1);
|
||||||
|
currentTrack = prev;
|
||||||
|
} else {
|
||||||
|
currentTrack = (currentTrack - 1 + trackTitles.length) % trackTitles.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayTrack(currentTrack);
|
||||||
|
buildUpNextList();
|
||||||
|
|
||||||
|
if (wasPlaying) {
|
||||||
|
togglePlayPause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSeekBar() {
|
||||||
|
int totalMillis = trackDurations[currentTrack] * 1000;
|
||||||
|
if (totalMillis > 0) {
|
||||||
|
int progress = (int) ((elapsedMillis / (float) totalMillis) * 1000);
|
||||||
|
seekBar.setProgress(Math.min(progress, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTimeLabels() {
|
||||||
|
int elapsedSec = elapsedMillis / 1000;
|
||||||
|
timeElapsed.setText(formatTime(elapsedSec));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTime(int totalSeconds) {
|
||||||
|
int minutes = totalSeconds / 60;
|
||||||
|
int seconds = totalSeconds % 60;
|
||||||
|
return minutes + ":" + String.format("%02d", seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
handler.removeCallbacks(playbackRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
340
android/app/src/main/java/com/aethex/os/NotesActivity.java
Normal file
340
android/app/src/main/java/com/aethex/os/NotesActivity.java
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class NotesActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "aethex_notes";
|
||||||
|
private static final String KEY_NOTES = "notes_json";
|
||||||
|
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private View listContainer;
|
||||||
|
private View editContainer;
|
||||||
|
private View emptyState;
|
||||||
|
private View fab;
|
||||||
|
private EditText editTitle;
|
||||||
|
private EditText editBody;
|
||||||
|
private TextView editTitleLabel;
|
||||||
|
|
||||||
|
private List<NoteItem> notes = new ArrayList<>();
|
||||||
|
private NotesAdapter adapter;
|
||||||
|
private int editingIndex = -1; // -1 = new note
|
||||||
|
|
||||||
|
// ── Lifecycle ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_notes);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
listContainer = findViewById(R.id.notes_list_container);
|
||||||
|
editContainer = findViewById(R.id.notes_edit_container);
|
||||||
|
emptyState = findViewById(R.id.notes_empty);
|
||||||
|
fab = findViewById(R.id.notes_fab);
|
||||||
|
recyclerView = findViewById(R.id.notes_recycler);
|
||||||
|
editTitle = findViewById(R.id.notes_edit_title);
|
||||||
|
editBody = findViewById(R.id.notes_edit_body);
|
||||||
|
editTitleLabel = findViewById(R.id.notes_edit_title_label);
|
||||||
|
|
||||||
|
// Apply Source Code Pro font (keeping amber/yellow aesthetic)
|
||||||
|
editBody.setTypeface(themeManager.getMonoFont(this));
|
||||||
|
|
||||||
|
// Load persisted notes
|
||||||
|
loadNotes();
|
||||||
|
|
||||||
|
// RecyclerView setup
|
||||||
|
adapter = new NotesAdapter();
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
// Swipe-to-delete
|
||||||
|
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0,
|
||||||
|
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
|
||||||
|
@Override
|
||||||
|
public boolean onMove(@NonNull RecyclerView rv,
|
||||||
|
@NonNull RecyclerView.ViewHolder vh,
|
||||||
|
@NonNull RecyclerView.ViewHolder target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwiped(@NonNull RecyclerView.ViewHolder vh, int direction) {
|
||||||
|
int pos = vh.getAdapterPosition();
|
||||||
|
if (pos >= 0 && pos < notes.size()) {
|
||||||
|
notes.remove(pos);
|
||||||
|
adapter.notifyItemRemoved(pos);
|
||||||
|
saveNotes();
|
||||||
|
updateEmptyState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).attachToRecyclerView(recyclerView);
|
||||||
|
|
||||||
|
// Back button (list view)
|
||||||
|
findViewById(R.id.notes_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FAB: new note
|
||||||
|
fab.setOnClickListener(v -> openEditor(-1));
|
||||||
|
|
||||||
|
// Edit view: back (discard)
|
||||||
|
findViewById(R.id.notes_edit_back).setOnClickListener(v -> closeEditor());
|
||||||
|
|
||||||
|
// Edit view: save
|
||||||
|
findViewById(R.id.notes_save_btn).setOnClickListener(v -> saveCurrentNote());
|
||||||
|
|
||||||
|
// Fade-in animation
|
||||||
|
View root = findViewById(R.id.notes_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
updateEmptyState();
|
||||||
|
|
||||||
|
// Custom themed keyboard
|
||||||
|
AeThexKeyboard.attach(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (editContainer.getVisibility() == View.VISIBLE) {
|
||||||
|
closeEditor();
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Editor logic ───────────────────────────────────────────
|
||||||
|
|
||||||
|
private void openEditor(int index) {
|
||||||
|
editingIndex = index;
|
||||||
|
if (index >= 0 && index < notes.size()) {
|
||||||
|
NoteItem note = notes.get(index);
|
||||||
|
editTitle.setText(note.title);
|
||||||
|
editBody.setText(note.body);
|
||||||
|
editTitleLabel.setText("Edit Note");
|
||||||
|
} else {
|
||||||
|
editTitle.setText("");
|
||||||
|
editBody.setText("");
|
||||||
|
editTitleLabel.setText("New Note");
|
||||||
|
}
|
||||||
|
|
||||||
|
listContainer.setVisibility(View.GONE);
|
||||||
|
fab.setVisibility(View.GONE);
|
||||||
|
editContainer.setVisibility(View.VISIBLE);
|
||||||
|
editContainer.setAlpha(0f);
|
||||||
|
editContainer.animate().alpha(1f).setDuration(200).start();
|
||||||
|
editTitle.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeEditor() {
|
||||||
|
editContainer.setVisibility(View.GONE);
|
||||||
|
listContainer.setVisibility(View.VISIBLE);
|
||||||
|
fab.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCurrentNote() {
|
||||||
|
String title = editTitle.getText().toString().trim();
|
||||||
|
String body = editBody.getText().toString().trim();
|
||||||
|
|
||||||
|
// Don't save completely empty notes
|
||||||
|
if (title.isEmpty() && body.isEmpty()) {
|
||||||
|
closeEditor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If title is empty, use the first line of the body
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
String[] lines = body.split("\n");
|
||||||
|
title = lines[0].length() > 50 ? lines[0].substring(0, 50) : lines[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
String now = new SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault())
|
||||||
|
.format(new Date());
|
||||||
|
|
||||||
|
if (editingIndex >= 0 && editingIndex < notes.size()) {
|
||||||
|
// Update existing
|
||||||
|
NoteItem note = notes.get(editingIndex);
|
||||||
|
note.title = title;
|
||||||
|
note.body = body;
|
||||||
|
note.date = now;
|
||||||
|
adapter.notifyItemChanged(editingIndex);
|
||||||
|
} else {
|
||||||
|
// Create new (insert at beginning)
|
||||||
|
NoteItem note = new NoteItem(title, body, now);
|
||||||
|
notes.add(0, note);
|
||||||
|
adapter.notifyItemInserted(0);
|
||||||
|
recyclerView.scrollToPosition(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveNotes();
|
||||||
|
updateEmptyState();
|
||||||
|
closeEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Persistence (SharedPreferences + JSON) ─────────────────
|
||||||
|
|
||||||
|
private void loadNotes() {
|
||||||
|
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
String json = prefs.getString(KEY_NOTES, "[]");
|
||||||
|
notes.clear();
|
||||||
|
try {
|
||||||
|
JSONArray arr = new JSONArray(json);
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONObject obj = arr.getJSONObject(i);
|
||||||
|
notes.add(new NoteItem(
|
||||||
|
obj.optString("title", ""),
|
||||||
|
obj.optString("body", ""),
|
||||||
|
obj.optString("date", "")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveNotes() {
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
try {
|
||||||
|
for (NoteItem n : notes) {
|
||||||
|
JSONObject obj = new JSONObject();
|
||||||
|
obj.put("title", n.title);
|
||||||
|
obj.put("body", n.body);
|
||||||
|
obj.put("date", n.date);
|
||||||
|
arr.put(obj);
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putString(KEY_NOTES, arr.toString())
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Empty state ────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateEmptyState() {
|
||||||
|
if (notes.isEmpty()) {
|
||||||
|
emptyState.setVisibility(View.VISIBLE);
|
||||||
|
recyclerView.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
emptyState.setVisibility(View.GONE);
|
||||||
|
recyclerView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Data model ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static class NoteItem {
|
||||||
|
String title;
|
||||||
|
String body;
|
||||||
|
String date;
|
||||||
|
|
||||||
|
NoteItem(String title, String body, String date) {
|
||||||
|
this.title = title;
|
||||||
|
this.body = body;
|
||||||
|
this.date = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RecyclerView Adapter ───────────────────────────────────
|
||||||
|
|
||||||
|
private class NotesAdapter extends RecyclerView.Adapter<NotesAdapter.VH> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View v = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_note, parent, false);
|
||||||
|
return new VH(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull VH holder, int position) {
|
||||||
|
NoteItem note = notes.get(position);
|
||||||
|
holder.title.setText(note.title);
|
||||||
|
holder.body.setText(note.body);
|
||||||
|
holder.date.setText(note.date);
|
||||||
|
|
||||||
|
// Hide body if empty
|
||||||
|
holder.body.setVisibility(
|
||||||
|
note.body.isEmpty() ? View.GONE : View.VISIBLE);
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener(v -> openEditor(holder.getAdapterPosition()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return notes.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class VH extends RecyclerView.ViewHolder {
|
||||||
|
TextView title, body, date;
|
||||||
|
|
||||||
|
VH(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
title = itemView.findViewById(R.id.note_item_title);
|
||||||
|
body = itemView.findViewById(R.id.note_item_body);
|
||||||
|
date = itemView.findViewById(R.id.note_item_date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Immersive mode ─────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
681
android/app/src/main/java/com/aethex/os/PassportActivity.java
Normal file
681
android/app/src/main/java/com/aethex/os/PassportActivity.java
Normal file
|
|
@ -0,0 +1,681 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passport — User identity card with edit, share, and QR code.
|
||||||
|
*/
|
||||||
|
public class PassportActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private Typeface displayFont;
|
||||||
|
private Typeface monoFont;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "aethex_passport";
|
||||||
|
|
||||||
|
// Editable fields
|
||||||
|
private TextView usernameView;
|
||||||
|
private TextView bioView;
|
||||||
|
private boolean editMode = false;
|
||||||
|
|
||||||
|
// Content parent for rebuilding
|
||||||
|
private LinearLayout contentParent;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
displayFont = themeManager.getDisplayFont(this);
|
||||||
|
monoFont = themeManager.getMonoFont(this);
|
||||||
|
prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
TextView title = findViewById(R.id.app_title);
|
||||||
|
title.setText("Passport");
|
||||||
|
TextView nameDisplay = findViewById(R.id.app_name_display);
|
||||||
|
|
||||||
|
findViewById(R.id.app_back).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
contentParent = (LinearLayout) nameDisplay.getParent();
|
||||||
|
contentParent.removeAllViews();
|
||||||
|
contentParent.setGravity(Gravity.TOP);
|
||||||
|
contentParent.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
|
buildPassportUI(contentParent);
|
||||||
|
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildPassportUI(LinearLayout parent) {
|
||||||
|
boolean isFoundation = themeManager.isFoundation();
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
|
||||||
|
ScrollView scroll = new ScrollView(this);
|
||||||
|
scroll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
scroll.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
|
||||||
|
LinearLayout wrapper = new LinearLayout(this);
|
||||||
|
wrapper.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
wrapper.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(24));
|
||||||
|
|
||||||
|
// ── Action buttons row ──
|
||||||
|
LinearLayout actionsRow = new LinearLayout(this);
|
||||||
|
actionsRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
actionsRow.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams arP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
arP.bottomMargin = dpToPx(12);
|
||||||
|
actionsRow.setLayoutParams(arP);
|
||||||
|
|
||||||
|
// Edit button
|
||||||
|
TextView editBtn = new TextView(this);
|
||||||
|
editBtn.setText("EDIT");
|
||||||
|
editBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
editBtn.setTextColor(primaryColor);
|
||||||
|
editBtn.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
editBtn.setLetterSpacing(0.1f);
|
||||||
|
editBtn.setPadding(dpToPx(14), dpToPx(6), dpToPx(14), dpToPx(6));
|
||||||
|
GradientDrawable editBg = new GradientDrawable();
|
||||||
|
editBg.setCornerRadius(dpToPx(6));
|
||||||
|
editBg.setStroke(dpToPx(1), primaryColor);
|
||||||
|
editBg.setColor(Color.parseColor("#0D000000"));
|
||||||
|
editBtn.setBackground(editBg);
|
||||||
|
editBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
toggleEditMode(editBtn);
|
||||||
|
});
|
||||||
|
actionsRow.addView(editBtn);
|
||||||
|
|
||||||
|
// Share button
|
||||||
|
TextView shareBtn = new TextView(this);
|
||||||
|
shareBtn.setText("SHARE");
|
||||||
|
shareBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
shareBtn.setTextColor(Color.parseColor("#99FFFFFF"));
|
||||||
|
shareBtn.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
shareBtn.setLetterSpacing(0.1f);
|
||||||
|
shareBtn.setPadding(dpToPx(14), dpToPx(6), dpToPx(14), dpToPx(6));
|
||||||
|
GradientDrawable shareBg = new GradientDrawable();
|
||||||
|
shareBg.setCornerRadius(dpToPx(6));
|
||||||
|
shareBg.setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"));
|
||||||
|
shareBg.setColor(Color.parseColor("#0D000000"));
|
||||||
|
shareBtn.setBackground(shareBg);
|
||||||
|
LinearLayout.LayoutParams sbP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
sbP.setMarginStart(dpToPx(8));
|
||||||
|
shareBtn.setLayoutParams(sbP);
|
||||||
|
shareBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
sharePassport();
|
||||||
|
});
|
||||||
|
actionsRow.addView(shareBtn);
|
||||||
|
|
||||||
|
wrapper.addView(actionsRow);
|
||||||
|
|
||||||
|
// ── Passport Card ──
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
card.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||||
|
card.setPadding(dpToPx(24), dpToPx(28), dpToPx(24), dpToPx(28));
|
||||||
|
|
||||||
|
GradientDrawable cardBg = new GradientDrawable();
|
||||||
|
cardBg.setCornerRadius(dpToPx(16));
|
||||||
|
cardBg.setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"));
|
||||||
|
cardBg.setColor(Color.parseColor("#1A0F172A"));
|
||||||
|
card.setBackground(cardBg);
|
||||||
|
|
||||||
|
// Avatar circle
|
||||||
|
FrameLayout avatar = new FrameLayout(this);
|
||||||
|
int avatarSize = dpToPx(72);
|
||||||
|
LinearLayout.LayoutParams avatarParams = new LinearLayout.LayoutParams(avatarSize, avatarSize);
|
||||||
|
avatarParams.gravity = Gravity.CENTER_HORIZONTAL;
|
||||||
|
avatar.setLayoutParams(avatarParams);
|
||||||
|
avatar.setBackgroundResource(themeManager.getBootLogoDrawable());
|
||||||
|
|
||||||
|
TextView avatarText = new TextView(this);
|
||||||
|
avatarText.setText("Æ");
|
||||||
|
avatarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 32);
|
||||||
|
avatarText.setTextColor(Color.WHITE);
|
||||||
|
avatarText.setTypeface(displayFont, Typeface.BOLD);
|
||||||
|
avatarText.setGravity(Gravity.CENTER);
|
||||||
|
avatar.addView(avatarText, new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
card.addView(avatar);
|
||||||
|
|
||||||
|
// Username (editable)
|
||||||
|
String savedName = prefs.getString("username", "ARCHITECT");
|
||||||
|
usernameView = new TextView(this);
|
||||||
|
usernameView.setText(savedName);
|
||||||
|
usernameView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 22);
|
||||||
|
usernameView.setTextColor(Color.WHITE);
|
||||||
|
usernameView.setTypeface(displayFont);
|
||||||
|
usernameView.setGravity(Gravity.CENTER);
|
||||||
|
usernameView.setLetterSpacing(0.1f);
|
||||||
|
LinearLayout.LayoutParams uParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
uParams.topMargin = dpToPx(16);
|
||||||
|
usernameView.setLayoutParams(uParams);
|
||||||
|
card.addView(usernameView);
|
||||||
|
|
||||||
|
// Bio (editable)
|
||||||
|
String savedBio = prefs.getString("bio", "System Architect · AeThex OS");
|
||||||
|
bioView = new TextView(this);
|
||||||
|
bioView.setText(savedBio);
|
||||||
|
bioView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
bioView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
bioView.setTypeface(monoFont);
|
||||||
|
bioView.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams bioP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
bioP.topMargin = dpToPx(4);
|
||||||
|
bioView.setLayoutParams(bioP);
|
||||||
|
card.addView(bioView);
|
||||||
|
|
||||||
|
// Clearance badge
|
||||||
|
TextView badge = new TextView(this);
|
||||||
|
badge.setText(isFoundation ? "FOUNDATION CLEARANCE" : "CORP CLEARANCE");
|
||||||
|
badge.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
badge.setTextColor(themeManager.getClearanceBadgeTextColor());
|
||||||
|
badge.setTypeface(monoFont);
|
||||||
|
badge.setLetterSpacing(0.15f);
|
||||||
|
badge.setGravity(Gravity.CENTER);
|
||||||
|
GradientDrawable badgeBg = new GradientDrawable();
|
||||||
|
badgeBg.setCornerRadius(dpToPx(4));
|
||||||
|
badgeBg.setColor(themeManager.getClearanceBadgeBackground());
|
||||||
|
badge.setBackground(badgeBg);
|
||||||
|
badge.setPadding(dpToPx(12), dpToPx(4), dpToPx(12), dpToPx(4));
|
||||||
|
LinearLayout.LayoutParams bParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
bParams.topMargin = dpToPx(10);
|
||||||
|
bParams.gravity = Gravity.CENTER_HORIZONTAL;
|
||||||
|
badge.setLayoutParams(bParams);
|
||||||
|
card.addView(badge);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
View divider = new View(this);
|
||||||
|
LinearLayout.LayoutParams dParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(1));
|
||||||
|
dParams.topMargin = dpToPx(20);
|
||||||
|
dParams.bottomMargin = dpToPx(16);
|
||||||
|
divider.setLayoutParams(dParams);
|
||||||
|
divider.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
card.addView(divider);
|
||||||
|
|
||||||
|
// Info rows
|
||||||
|
addInfoRow(card, "ID", "AX-2847-ARCH");
|
||||||
|
addInfoRow(card, "STATUS", "ACTIVE");
|
||||||
|
addInfoRow(card, "JOINED", "2024-01-15");
|
||||||
|
addInfoRow(card, "ACCESS", "Level 5");
|
||||||
|
addInfoRow(card, "REGION", "Sector 7-G");
|
||||||
|
addInfoRow(card, "MODULES", "12 active");
|
||||||
|
|
||||||
|
wrapper.addView(card);
|
||||||
|
|
||||||
|
// ── QR Code Section ──
|
||||||
|
TextView qrLabel = new TextView(this);
|
||||||
|
qrLabel.setText("IDENTITY QR");
|
||||||
|
qrLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
qrLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
qrLabel.setTypeface(monoFont);
|
||||||
|
qrLabel.setLetterSpacing(0.15f);
|
||||||
|
qrLabel.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams qlP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
qlP.topMargin = dpToPx(24);
|
||||||
|
qlP.bottomMargin = dpToPx(8);
|
||||||
|
qrLabel.setLayoutParams(qlP);
|
||||||
|
wrapper.addView(qrLabel);
|
||||||
|
|
||||||
|
// QR code (simulated with a grid pattern)
|
||||||
|
LinearLayout qrCard = new LinearLayout(this);
|
||||||
|
qrCard.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
qrCard.setGravity(Gravity.CENTER);
|
||||||
|
qrCard.setPadding(dpToPx(20), dpToPx(20), dpToPx(20), dpToPx(20));
|
||||||
|
LinearLayout.LayoutParams qcP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
qrCard.setLayoutParams(qcP);
|
||||||
|
|
||||||
|
GradientDrawable qrCardBg = new GradientDrawable();
|
||||||
|
qrCardBg.setCornerRadius(dpToPx(12));
|
||||||
|
qrCardBg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
qrCardBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
qrCard.setBackground(qrCardBg);
|
||||||
|
|
||||||
|
// Generate a deterministic pseudo-QR grid
|
||||||
|
ImageView qrImage = new ImageView(this);
|
||||||
|
int qrSize = dpToPx(140);
|
||||||
|
LinearLayout.LayoutParams qrIp = new LinearLayout.LayoutParams(qrSize, qrSize);
|
||||||
|
qrIp.gravity = Gravity.CENTER_HORIZONTAL;
|
||||||
|
qrImage.setLayoutParams(qrIp);
|
||||||
|
qrImage.setImageBitmap(generateQrBitmap(savedName, primaryColor));
|
||||||
|
qrCard.addView(qrImage);
|
||||||
|
|
||||||
|
// QR text below
|
||||||
|
TextView qrText = new TextView(this);
|
||||||
|
qrText.setText("Scan to verify identity");
|
||||||
|
qrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
qrText.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
qrText.setTypeface(monoFont);
|
||||||
|
qrText.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams qtP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
qtP.topMargin = dpToPx(10);
|
||||||
|
qrText.setLayoutParams(qtP);
|
||||||
|
qrCard.addView(qrText);
|
||||||
|
|
||||||
|
// Tap QR to copy ID
|
||||||
|
qrCard.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("AeThex ID", "AX-2847-ARCH"));
|
||||||
|
AeThexToast.show(this, "ID copied to clipboard", AeThexToast.Type.SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.addView(qrCard);
|
||||||
|
|
||||||
|
// ── Stats Section ──
|
||||||
|
TextView statsLabel = new TextView(this);
|
||||||
|
statsLabel.setText("ACTIVITY");
|
||||||
|
statsLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
statsLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
statsLabel.setTypeface(monoFont);
|
||||||
|
statsLabel.setLetterSpacing(0.15f);
|
||||||
|
LinearLayout.LayoutParams slP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
slP.topMargin = dpToPx(24);
|
||||||
|
slP.bottomMargin = dpToPx(8);
|
||||||
|
statsLabel.setLayoutParams(slP);
|
||||||
|
wrapper.addView(statsLabel);
|
||||||
|
|
||||||
|
// Stats grid
|
||||||
|
LinearLayout statsRow = new LinearLayout(this);
|
||||||
|
statsRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
statsRow.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
addStatTile(statsRow, "42", "Sessions", "#06B6D4");
|
||||||
|
addStatTile(statsRow, "187", "Commands", "#22C55E");
|
||||||
|
addStatTile(statsRow, "12", "Projects", "#A855F7");
|
||||||
|
addStatTile(statsRow, "5", "Badges", "#F97316");
|
||||||
|
|
||||||
|
wrapper.addView(statsRow);
|
||||||
|
|
||||||
|
scroll.addView(wrapper);
|
||||||
|
parent.addView(scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addInfoRow(LinearLayout parent, String label, String value) {
|
||||||
|
LinearLayout row = new LinearLayout(this);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rowParams.bottomMargin = dpToPx(8);
|
||||||
|
row.setLayoutParams(rowParams);
|
||||||
|
|
||||||
|
TextView labelView = new TextView(this);
|
||||||
|
labelView.setText(label);
|
||||||
|
labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
labelView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
labelView.setTypeface(monoFont);
|
||||||
|
labelView.setLetterSpacing(0.1f);
|
||||||
|
labelView.setLayoutParams(new LinearLayout.LayoutParams(dpToPx(80), ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
row.addView(labelView);
|
||||||
|
|
||||||
|
TextView valueView = new TextView(this);
|
||||||
|
valueView.setText(value);
|
||||||
|
valueView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
valueView.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
valueView.setTypeface(monoFont);
|
||||||
|
row.addView(valueView);
|
||||||
|
|
||||||
|
parent.addView(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addStatTile(LinearLayout parent, String value, String label, String colorHex) {
|
||||||
|
LinearLayout tile = new LinearLayout(this);
|
||||||
|
tile.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
tile.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams tp = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
tp.setMarginEnd(dpToPx(6));
|
||||||
|
tile.setLayoutParams(tp);
|
||||||
|
tile.setPadding(dpToPx(8), dpToPx(12), dpToPx(8), dpToPx(12));
|
||||||
|
|
||||||
|
GradientDrawable tileBg = new GradientDrawable();
|
||||||
|
tileBg.setCornerRadius(dpToPx(10));
|
||||||
|
tileBg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
tileBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
tile.setBackground(tileBg);
|
||||||
|
|
||||||
|
TextView valView = new TextView(this);
|
||||||
|
valView.setText(value);
|
||||||
|
valView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
|
||||||
|
valView.setTextColor(Color.parseColor(colorHex));
|
||||||
|
valView.setTypeface(displayFont);
|
||||||
|
valView.setGravity(Gravity.CENTER);
|
||||||
|
tile.addView(valView);
|
||||||
|
|
||||||
|
TextView labView = new TextView(this);
|
||||||
|
labView.setText(label);
|
||||||
|
labView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
labView.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
labView.setTypeface(monoFont);
|
||||||
|
labView.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
lp.topMargin = dpToPx(4);
|
||||||
|
labView.setLayoutParams(lp);
|
||||||
|
tile.addView(labView);
|
||||||
|
|
||||||
|
parent.addView(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private android.graphics.Bitmap generateQrBitmap(String seed, int accentColor) {
|
||||||
|
int gridSize = 21; // QR-like grid
|
||||||
|
int cellPx = dpToPx(140) / gridSize;
|
||||||
|
int bmpSize = cellPx * gridSize;
|
||||||
|
|
||||||
|
android.graphics.Bitmap bmp = android.graphics.Bitmap.createBitmap(
|
||||||
|
bmpSize, bmpSize, android.graphics.Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(bmp);
|
||||||
|
canvas.drawColor(Color.WHITE);
|
||||||
|
|
||||||
|
Paint paint = new Paint();
|
||||||
|
paint.setAntiAlias(false);
|
||||||
|
|
||||||
|
// Deterministic pseudo-random from seed
|
||||||
|
int hash = seed.hashCode();
|
||||||
|
java.util.Random rand = new java.util.Random(hash);
|
||||||
|
|
||||||
|
// Draw finder patterns (3 corners)
|
||||||
|
drawFinderPattern(canvas, paint, 0, 0, cellPx, accentColor);
|
||||||
|
drawFinderPattern(canvas, paint, (gridSize - 7) * cellPx, 0, cellPx, accentColor);
|
||||||
|
drawFinderPattern(canvas, paint, 0, (gridSize - 7) * cellPx, cellPx, accentColor);
|
||||||
|
|
||||||
|
// Fill data area with pseudo-random pattern
|
||||||
|
paint.setColor(Color.parseColor("#1A1A2E"));
|
||||||
|
for (int y = 0; y < gridSize; y++) {
|
||||||
|
for (int x = 0; x < gridSize; x++) {
|
||||||
|
// Skip finder pattern areas
|
||||||
|
if ((x < 8 && y < 8) || (x >= gridSize - 8 && y < 8) || (x < 8 && y >= gridSize - 8))
|
||||||
|
continue;
|
||||||
|
if (rand.nextFloat() < 0.45f) {
|
||||||
|
canvas.drawRect(x * cellPx, y * cellPx,
|
||||||
|
(x + 1) * cellPx, (y + 1) * cellPx, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawFinderPattern(Canvas canvas, Paint paint, int x, int y, int cell, int color) {
|
||||||
|
// Outer ring
|
||||||
|
paint.setColor(Color.parseColor("#1A1A2E"));
|
||||||
|
canvas.drawRect(x, y, x + 7 * cell, y + 7 * cell, paint);
|
||||||
|
// White inner
|
||||||
|
paint.setColor(Color.WHITE);
|
||||||
|
canvas.drawRect(x + cell, y + cell, x + 6 * cell, y + 6 * cell, paint);
|
||||||
|
// Center block with accent
|
||||||
|
paint.setColor(color);
|
||||||
|
canvas.drawRect(x + 2 * cell, y + 2 * cell, x + 5 * cell, y + 5 * cell, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleEditMode(TextView editBtn) {
|
||||||
|
if (!editMode) {
|
||||||
|
// Enter edit mode — show edit overlay
|
||||||
|
editMode = true;
|
||||||
|
editBtn.setText("DONE");
|
||||||
|
showEditOverlay();
|
||||||
|
} else {
|
||||||
|
// Save and exit edit mode
|
||||||
|
editMode = false;
|
||||||
|
editBtn.setText("EDIT");
|
||||||
|
dismissEditOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEditOverlay() {
|
||||||
|
FrameLayout root = findViewById(R.id.app_root);
|
||||||
|
|
||||||
|
FrameLayout scrim = new FrameLayout(this);
|
||||||
|
scrim.setTag("passport_edit_overlay");
|
||||||
|
scrim.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
scrim.setBackgroundColor(Color.parseColor("#CC000000"));
|
||||||
|
|
||||||
|
LinearLayout panel = new LinearLayout(this);
|
||||||
|
panel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
int panelWidth = dpToPx(300);
|
||||||
|
FrameLayout.LayoutParams pp = new FrameLayout.LayoutParams(
|
||||||
|
panelWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
pp.gravity = Gravity.CENTER;
|
||||||
|
panel.setLayoutParams(pp);
|
||||||
|
panel.setPadding(dpToPx(20), dpToPx(20), dpToPx(20), dpToPx(20));
|
||||||
|
|
||||||
|
GradientDrawable panelBg = new GradientDrawable();
|
||||||
|
panelBg.setCornerRadius(dpToPx(16));
|
||||||
|
panelBg.setColor(Color.parseColor("#E6111827"));
|
||||||
|
panelBg.setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"));
|
||||||
|
panel.setBackground(panelBg);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
TextView title = new TextView(this);
|
||||||
|
title.setText("EDIT PASSPORT");
|
||||||
|
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
title.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
title.setTypeface(monoFont);
|
||||||
|
title.setLetterSpacing(0.15f);
|
||||||
|
LinearLayout.LayoutParams tP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
tP.bottomMargin = dpToPx(16);
|
||||||
|
title.setLayoutParams(tP);
|
||||||
|
panel.addView(title);
|
||||||
|
|
||||||
|
// Username field
|
||||||
|
TextView nameLabel = new TextView(this);
|
||||||
|
nameLabel.setText("CALLSIGN");
|
||||||
|
nameLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
nameLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
nameLabel.setTypeface(monoFont);
|
||||||
|
panel.addView(nameLabel);
|
||||||
|
|
||||||
|
EditText nameInput = new EditText(this);
|
||||||
|
nameInput.setText(prefs.getString("username", "ARCHITECT"));
|
||||||
|
nameInput.setTextColor(Color.WHITE);
|
||||||
|
nameInput.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
nameInput.setTypeface(monoFont);
|
||||||
|
nameInput.setSingleLine(true);
|
||||||
|
nameInput.setPadding(dpToPx(12), dpToPx(10), dpToPx(12), dpToPx(10));
|
||||||
|
nameInput.setShowSoftInputOnFocus(false);
|
||||||
|
GradientDrawable niBg = new GradientDrawable();
|
||||||
|
niBg.setCornerRadius(dpToPx(8));
|
||||||
|
niBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
niBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
nameInput.setBackground(niBg);
|
||||||
|
LinearLayout.LayoutParams niP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
niP.topMargin = dpToPx(4);
|
||||||
|
niP.bottomMargin = dpToPx(14);
|
||||||
|
nameInput.setLayoutParams(niP);
|
||||||
|
nameInput.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus) AeThexKeyboard.attachToEditText(this, nameInput);
|
||||||
|
});
|
||||||
|
panel.addView(nameInput);
|
||||||
|
|
||||||
|
// Bio field
|
||||||
|
TextView bioLabel = new TextView(this);
|
||||||
|
bioLabel.setText("BIO");
|
||||||
|
bioLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
bioLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
bioLabel.setTypeface(monoFont);
|
||||||
|
panel.addView(bioLabel);
|
||||||
|
|
||||||
|
EditText bioInput = new EditText(this);
|
||||||
|
bioInput.setText(prefs.getString("bio", "System Architect · AeThex OS"));
|
||||||
|
bioInput.setTextColor(Color.WHITE);
|
||||||
|
bioInput.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
bioInput.setTypeface(monoFont);
|
||||||
|
bioInput.setMaxLines(3);
|
||||||
|
bioInput.setPadding(dpToPx(12), dpToPx(10), dpToPx(12), dpToPx(10));
|
||||||
|
bioInput.setShowSoftInputOnFocus(false);
|
||||||
|
GradientDrawable biBg = new GradientDrawable();
|
||||||
|
biBg.setCornerRadius(dpToPx(8));
|
||||||
|
biBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
biBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
bioInput.setBackground(biBg);
|
||||||
|
LinearLayout.LayoutParams biP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
biP.topMargin = dpToPx(4);
|
||||||
|
biP.bottomMargin = dpToPx(20);
|
||||||
|
bioInput.setLayoutParams(biP);
|
||||||
|
bioInput.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus) AeThexKeyboard.attachToEditText(this, bioInput);
|
||||||
|
});
|
||||||
|
panel.addView(bioInput);
|
||||||
|
|
||||||
|
// Save button
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
TextView saveBtn = new TextView(this);
|
||||||
|
saveBtn.setText("SAVE CHANGES");
|
||||||
|
saveBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
saveBtn.setTextColor(Color.WHITE);
|
||||||
|
saveBtn.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
saveBtn.setGravity(Gravity.CENTER);
|
||||||
|
saveBtn.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(12));
|
||||||
|
GradientDrawable saveBg = new GradientDrawable();
|
||||||
|
saveBg.setCornerRadius(dpToPx(8));
|
||||||
|
saveBg.setColor(primaryColor);
|
||||||
|
saveBtn.setBackground(saveBg);
|
||||||
|
saveBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
String newName = nameInput.getText().toString().trim();
|
||||||
|
String newBio = bioInput.getText().toString().trim();
|
||||||
|
if (!newName.isEmpty()) {
|
||||||
|
prefs.edit()
|
||||||
|
.putString("username", newName.toUpperCase())
|
||||||
|
.putString("bio", newBio)
|
||||||
|
.apply();
|
||||||
|
usernameView.setText(newName.toUpperCase());
|
||||||
|
bioView.setText(newBio);
|
||||||
|
}
|
||||||
|
AeThexKeyboard.dismissKeyboard(this);
|
||||||
|
editMode = false;
|
||||||
|
dismissEditOverlay();
|
||||||
|
AeThexToast.show(this, "Passport updated", AeThexToast.Type.SUCCESS);
|
||||||
|
});
|
||||||
|
panel.addView(saveBtn);
|
||||||
|
|
||||||
|
scrim.addView(panel);
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
root.addView(scrim);
|
||||||
|
scrim.animate().alpha(1f).setDuration(200).start();
|
||||||
|
|
||||||
|
// Focus name input
|
||||||
|
nameInput.requestFocus();
|
||||||
|
nameInput.postDelayed(() -> AeThexKeyboard.attachToEditText(this, nameInput), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissEditOverlay() {
|
||||||
|
AeThexKeyboard.dismissKeyboard(this);
|
||||||
|
FrameLayout root = findViewById(R.id.app_root);
|
||||||
|
View overlay = root.findViewWithTag("passport_edit_overlay");
|
||||||
|
if (overlay != null) {
|
||||||
|
overlay.animate().alpha(0f).setDuration(150).withEndAction(() -> {
|
||||||
|
root.removeView(overlay);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sharePassport() {
|
||||||
|
String name = prefs.getString("username", "ARCHITECT");
|
||||||
|
String bio = prefs.getString("bio", "System Architect · AeThex OS");
|
||||||
|
String clearance = themeManager.isFoundation() ? "Foundation" : "Corp";
|
||||||
|
|
||||||
|
String shareText = "AeThex Passport\n" +
|
||||||
|
"Callsign: " + name + "\n" +
|
||||||
|
"Bio: " + bio + "\n" +
|
||||||
|
"ID: AX-2847-ARCH\n" +
|
||||||
|
"Clearance: " + clearance + "\n" +
|
||||||
|
"Status: ACTIVE";
|
||||||
|
|
||||||
|
android.content.Intent shareIntent = new android.content.Intent(android.content.Intent.ACTION_SEND);
|
||||||
|
shareIntent.setType("text/plain");
|
||||||
|
shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareText);
|
||||||
|
startActivity(android.content.Intent.createChooser(shareIntent, "Share Passport"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (editMode) {
|
||||||
|
editMode = false;
|
||||||
|
AeThexKeyboard.dismissKeyboard(this);
|
||||||
|
dismissEditOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
482
android/app/src/main/java/com/aethex/os/PhotosActivity.java
Normal file
482
android/app/src/main/java/com/aethex/os/PhotosActivity.java
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PhotosActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private FrameLayout viewerOverlay;
|
||||||
|
private View viewerPhoto;
|
||||||
|
private TextView viewerFilename;
|
||||||
|
private TextView viewerSize;
|
||||||
|
private TextView viewerDate;
|
||||||
|
private float viewerStartY;
|
||||||
|
private List<PhotoItem> photos;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_photos);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
findViewById(R.id.photos_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter tabs
|
||||||
|
findViewById(R.id.tab_favorites).setOnClickListener(v ->
|
||||||
|
Toast.makeText(this, "Favorites - Coming soon", Toast.LENGTH_SHORT).show());
|
||||||
|
findViewById(R.id.tab_recent).setOnClickListener(v ->
|
||||||
|
Toast.makeText(this, "Recent - Coming soon", Toast.LENGTH_SHORT).show());
|
||||||
|
|
||||||
|
// Generate photo data
|
||||||
|
photos = generatePhotos();
|
||||||
|
|
||||||
|
// Setup RecyclerView grid
|
||||||
|
RecyclerView grid = findViewById(R.id.photos_grid);
|
||||||
|
GridLayoutManager layoutManager = new GridLayoutManager(this, 3);
|
||||||
|
grid.setLayoutManager(layoutManager);
|
||||||
|
grid.addItemDecoration(new GridSpacingDecoration(3, dpToPx(4)));
|
||||||
|
grid.setAdapter(new PhotoAdapter());
|
||||||
|
|
||||||
|
// Fullscreen viewer setup
|
||||||
|
viewerOverlay = findViewById(R.id.photo_viewer_overlay);
|
||||||
|
viewerPhoto = findViewById(R.id.viewer_photo);
|
||||||
|
viewerFilename = findViewById(R.id.viewer_filename);
|
||||||
|
viewerSize = findViewById(R.id.viewer_size);
|
||||||
|
viewerDate = findViewById(R.id.viewer_date);
|
||||||
|
|
||||||
|
// Tap to dismiss viewer
|
||||||
|
viewerOverlay.setOnClickListener(v -> dismissViewer());
|
||||||
|
|
||||||
|
// Swipe down to dismiss
|
||||||
|
viewerOverlay.setOnTouchListener((v, event) -> {
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
viewerStartY = event.getY();
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
float dy = event.getY() - viewerStartY;
|
||||||
|
if (dy > 0) {
|
||||||
|
viewerOverlay.setTranslationY(dy * 0.5f);
|
||||||
|
float alpha = Math.max(0f, 1f - (dy / 800f));
|
||||||
|
viewerOverlay.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
float finalDy = event.getY() - viewerStartY;
|
||||||
|
if (finalDy > 150) {
|
||||||
|
dismissViewer();
|
||||||
|
} else {
|
||||||
|
viewerOverlay.animate()
|
||||||
|
.translationY(0f)
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(200)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View root = findViewById(R.id.photos_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PhotoItem> generatePhotos() {
|
||||||
|
List<PhotoItem> list = new ArrayList<>();
|
||||||
|
|
||||||
|
// 24 unique gradient pairs
|
||||||
|
int[][] gradients = {
|
||||||
|
{0xFF06B6D4, 0xFF7C3AED}, // cyan -> violet
|
||||||
|
{0xFFEF4444, 0xFFF97316}, // red -> orange
|
||||||
|
{0xFF10B981, 0xFF14B8A6}, // emerald -> teal
|
||||||
|
{0xFF3B82F6, 0xFF6366F1}, // blue -> indigo
|
||||||
|
{0xFFEC4899, 0xFFF43F5E}, // pink -> rose
|
||||||
|
{0xFFEAB308, 0xFFF59E0B}, // yellow -> amber
|
||||||
|
{0xFF8B5CF6, 0xFFD946EF}, // violet -> fuchsia
|
||||||
|
{0xFF06B6D4, 0xFF22D3EE}, // cyan -> sky
|
||||||
|
{0xFFEF4444, 0xFFEC4899}, // red -> pink
|
||||||
|
{0xFF10B981, 0xFF06B6D4}, // emerald -> cyan
|
||||||
|
{0xFF6366F1, 0xFFA855F7}, // indigo -> purple
|
||||||
|
{0xFFF97316, 0xFFEAB308}, // orange -> yellow
|
||||||
|
{0xFFF43F5E, 0xFF8B5CF6}, // rose -> violet
|
||||||
|
{0xFF14B8A6, 0xFF3B82F6}, // teal -> blue
|
||||||
|
{0xFFD946EF, 0xFFEF4444}, // fuchsia -> red
|
||||||
|
{0xFF22D3EE, 0xFF10B981}, // sky -> emerald
|
||||||
|
{0xFFA855F7, 0xFF06B6D4}, // purple -> cyan
|
||||||
|
{0xFFF59E0B, 0xFFEF4444}, // amber -> red
|
||||||
|
{0xFF7C3AED, 0xFF3B82F6}, // violet -> blue
|
||||||
|
{0xFFEC4899, 0xFFF59E0B}, // pink -> amber
|
||||||
|
{0xFF3B82F6, 0xFF10B981}, // blue -> emerald
|
||||||
|
{0xFFEAB308, 0xFF10B981}, // yellow -> emerald
|
||||||
|
{0xFF6366F1, 0xFFEC4899}, // indigo -> pink
|
||||||
|
{0xFFF43F5E, 0xFFF97316}, // rose -> orange
|
||||||
|
};
|
||||||
|
|
||||||
|
String[] months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
||||||
|
|
||||||
|
String[] resolutions = {
|
||||||
|
"4032 x 3024", "3840 x 2160", "3024 x 4032", "2560 x 1440",
|
||||||
|
"4000 x 3000", "3264 x 2448", "4032 x 3024", "1920 x 1080"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 24; i++) {
|
||||||
|
int num = 1001 + i;
|
||||||
|
String filename = "IMG_" + num + ".jpg";
|
||||||
|
|
||||||
|
// Distribute dates across recent months
|
||||||
|
int monthIdx = 11 - (i / 4); // Dec, Nov, Oct...
|
||||||
|
if (monthIdx < 0) monthIdx = 0;
|
||||||
|
int day = (i % 28) + 1;
|
||||||
|
int year = monthIdx >= 6 ? 2025 : 2025;
|
||||||
|
String date = months[monthIdx] + " " + day + ", " + year;
|
||||||
|
|
||||||
|
String resolution = resolutions[i % resolutions.length];
|
||||||
|
|
||||||
|
list.add(new PhotoItem(filename, date, resolution,
|
||||||
|
gradients[i][0], gradients[i][1], i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showViewer(PhotoItem photo) {
|
||||||
|
// Set photo gradient as fullscreen background
|
||||||
|
GradientDrawable bg = new GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.TL_BR,
|
||||||
|
new int[]{photo.colorStart, photo.colorEnd}
|
||||||
|
);
|
||||||
|
viewerPhoto.setBackground(bg);
|
||||||
|
|
||||||
|
viewerFilename.setText(photo.filename);
|
||||||
|
viewerSize.setText(photo.resolution);
|
||||||
|
viewerDate.setText(photo.date);
|
||||||
|
|
||||||
|
viewerOverlay.setTranslationY(0f);
|
||||||
|
viewerOverlay.setAlpha(0f);
|
||||||
|
viewerOverlay.setVisibility(View.VISIBLE);
|
||||||
|
viewerOverlay.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(250)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissViewer() {
|
||||||
|
viewerOverlay.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.translationY(100f)
|
||||||
|
.setDuration(200)
|
||||||
|
.setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
viewerOverlay.setVisibility(View.GONE);
|
||||||
|
viewerOverlay.setTranslationY(0f);
|
||||||
|
viewerOverlay.animate().setListener(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Inner data class ----
|
||||||
|
|
||||||
|
private static class PhotoItem {
|
||||||
|
String filename;
|
||||||
|
String date;
|
||||||
|
String resolution;
|
||||||
|
int colorStart;
|
||||||
|
int colorEnd;
|
||||||
|
int index;
|
||||||
|
|
||||||
|
PhotoItem(String filename, String date, String resolution,
|
||||||
|
int colorStart, int colorEnd, int index) {
|
||||||
|
this.filename = filename;
|
||||||
|
this.date = date;
|
||||||
|
this.resolution = resolution;
|
||||||
|
this.colorStart = colorStart;
|
||||||
|
this.colorEnd = colorEnd;
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RecyclerView Adapter ----
|
||||||
|
|
||||||
|
private class PhotoAdapter extends RecyclerView.Adapter<PhotoAdapter.PhotoViewHolder> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public PhotoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
// Create item view programmatically: a FrameLayout containing
|
||||||
|
// - the gradient View (fills the cell)
|
||||||
|
// - a subtle overlay pattern
|
||||||
|
FrameLayout container = new FrameLayout(parent.getContext());
|
||||||
|
container.setLayoutParams(new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
return new PhotoViewHolder(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull PhotoViewHolder holder, int position) {
|
||||||
|
PhotoItem photo = photos.get(position);
|
||||||
|
FrameLayout container = (FrameLayout) holder.itemView;
|
||||||
|
container.removeAllViews();
|
||||||
|
|
||||||
|
// Make it square
|
||||||
|
container.post(() -> {
|
||||||
|
int width = container.getWidth();
|
||||||
|
if (width > 0) {
|
||||||
|
ViewGroup.LayoutParams lp = container.getLayoutParams();
|
||||||
|
lp.height = width;
|
||||||
|
container.setLayoutParams(lp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main gradient background with rounded corners
|
||||||
|
GradientDrawable gradient = new GradientDrawable(
|
||||||
|
getGradientOrientation(position),
|
||||||
|
new int[]{photo.colorStart, photo.colorEnd}
|
||||||
|
);
|
||||||
|
gradient.setCornerRadius(dpToPx(8));
|
||||||
|
container.setBackground(gradient);
|
||||||
|
|
||||||
|
// Add a semi-transparent overlay pattern for depth
|
||||||
|
View overlay = new View(container.getContext());
|
||||||
|
overlay.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
GradientDrawable overlayGrad = new GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.BOTTOM_TOP,
|
||||||
|
new int[]{0x40000000, 0x00000000, 0x00000000, 0x20000000}
|
||||||
|
);
|
||||||
|
overlayGrad.setCornerRadius(dpToPx(8));
|
||||||
|
overlay.setBackground(overlayGrad);
|
||||||
|
container.addView(overlay);
|
||||||
|
|
||||||
|
// Add diagonal accent line for visual interest
|
||||||
|
if (position % 3 == 0) {
|
||||||
|
View accent = new View(container.getContext());
|
||||||
|
FrameLayout.LayoutParams accentLp = new FrameLayout.LayoutParams(
|
||||||
|
dpToPx(40), dpToPx(2));
|
||||||
|
accentLp.topMargin = dpToPx(16);
|
||||||
|
accentLp.leftMargin = dpToPx(12);
|
||||||
|
accent.setLayoutParams(accentLp);
|
||||||
|
accent.setAlpha(0.3f);
|
||||||
|
GradientDrawable accentBg = new GradientDrawable();
|
||||||
|
accentBg.setColor(0xFFFFFFFF);
|
||||||
|
accentBg.setCornerRadius(dpToPx(1));
|
||||||
|
accent.setBackground(accentBg);
|
||||||
|
container.addView(accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add small dot pattern for texture on some photos
|
||||||
|
if (position % 4 == 1) {
|
||||||
|
for (int r = 0; r < 3; r++) {
|
||||||
|
for (int c = 0; c < 3; c++) {
|
||||||
|
View dot = new View(container.getContext());
|
||||||
|
int dotSize = dpToPx(3);
|
||||||
|
FrameLayout.LayoutParams dotLp = new FrameLayout.LayoutParams(
|
||||||
|
dotSize, dotSize);
|
||||||
|
dotLp.topMargin = dpToPx(14 + r * 8);
|
||||||
|
dotLp.leftMargin = dpToPx(14 + c * 8);
|
||||||
|
dot.setLayoutParams(dotLp);
|
||||||
|
dot.setAlpha(0.2f);
|
||||||
|
GradientDrawable dotBg = new GradientDrawable();
|
||||||
|
dotBg.setColor(0xFFFFFFFF);
|
||||||
|
dotBg.setShape(GradientDrawable.OVAL);
|
||||||
|
dot.setBackground(dotBg);
|
||||||
|
container.addView(dot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ring overlay on some photos
|
||||||
|
if (position % 5 == 2) {
|
||||||
|
View ring = new View(container.getContext());
|
||||||
|
int ringSize = dpToPx(32);
|
||||||
|
FrameLayout.LayoutParams ringLp = new FrameLayout.LayoutParams(
|
||||||
|
ringSize, ringSize);
|
||||||
|
ringLp.topMargin = dpToPx(20);
|
||||||
|
ringLp.leftMargin = dpToPx(20);
|
||||||
|
ring.setLayoutParams(ringLp);
|
||||||
|
ring.setAlpha(0.2f);
|
||||||
|
GradientDrawable ringBg = new GradientDrawable();
|
||||||
|
ringBg.setShape(GradientDrawable.OVAL);
|
||||||
|
ringBg.setColor(0x00000000);
|
||||||
|
ringBg.setStroke(dpToPx(2), 0xFFFFFFFF);
|
||||||
|
ring.setBackground(ringBg);
|
||||||
|
container.addView(ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add triangle-like shape on some photos
|
||||||
|
if (position % 6 == 3) {
|
||||||
|
View tri1 = new View(container.getContext());
|
||||||
|
FrameLayout.LayoutParams triLp = new FrameLayout.LayoutParams(
|
||||||
|
dpToPx(24), dpToPx(1));
|
||||||
|
triLp.topMargin = dpToPx(24);
|
||||||
|
triLp.leftMargin = dpToPx(16);
|
||||||
|
tri1.setLayoutParams(triLp);
|
||||||
|
tri1.setAlpha(0.25f);
|
||||||
|
tri1.setBackgroundColor(0xFFFFFFFF);
|
||||||
|
container.addView(tri1);
|
||||||
|
|
||||||
|
View tri2 = new View(container.getContext());
|
||||||
|
FrameLayout.LayoutParams tri2Lp = new FrameLayout.LayoutParams(
|
||||||
|
dpToPx(1), dpToPx(20));
|
||||||
|
tri2Lp.topMargin = dpToPx(24);
|
||||||
|
tri2Lp.leftMargin = dpToPx(16);
|
||||||
|
tri2.setLayoutParams(tri2Lp);
|
||||||
|
tri2.setAlpha(0.25f);
|
||||||
|
tri2.setBackgroundColor(0xFFFFFFFF);
|
||||||
|
container.addView(tri2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip to rounded corners
|
||||||
|
container.setClipToOutline(true);
|
||||||
|
container.setOutlineProvider(new android.view.ViewOutlineProvider() {
|
||||||
|
@Override
|
||||||
|
public void getOutline(View view, android.graphics.Outline outline) {
|
||||||
|
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), dpToPx(8));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click handler to open viewer
|
||||||
|
container.setOnClickListener(v -> {
|
||||||
|
v.animate().scaleX(0.93f).scaleY(0.93f).setDuration(80).withEndAction(() -> {
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(80).start();
|
||||||
|
showViewer(photo);
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stagger fade-in animation
|
||||||
|
container.setAlpha(0f);
|
||||||
|
container.setTranslationY(20f);
|
||||||
|
container.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.translationY(0f)
|
||||||
|
.setDuration(300)
|
||||||
|
.setStartDelay(position * 30L)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return photos.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GradientDrawable.Orientation getGradientOrientation(int position) {
|
||||||
|
// Vary gradient direction for visual variety
|
||||||
|
switch (position % 6) {
|
||||||
|
case 0: return GradientDrawable.Orientation.TL_BR;
|
||||||
|
case 1: return GradientDrawable.Orientation.TOP_BOTTOM;
|
||||||
|
case 2: return GradientDrawable.Orientation.TR_BL;
|
||||||
|
case 3: return GradientDrawable.Orientation.LEFT_RIGHT;
|
||||||
|
case 4: return GradientDrawable.Orientation.BL_TR;
|
||||||
|
case 5: return GradientDrawable.Orientation.BOTTOM_TOP;
|
||||||
|
default: return GradientDrawable.Orientation.TL_BR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PhotoViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
PhotoViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Grid spacing item decoration ----
|
||||||
|
|
||||||
|
private static class GridSpacingDecoration extends RecyclerView.ItemDecoration {
|
||||||
|
private final int spanCount;
|
||||||
|
private final int spacing;
|
||||||
|
|
||||||
|
GridSpacingDecoration(int spanCount, int spacing) {
|
||||||
|
this.spanCount = spanCount;
|
||||||
|
this.spacing = spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getItemOffsets(@NonNull android.graphics.Rect outRect,
|
||||||
|
@NonNull View view,
|
||||||
|
@NonNull RecyclerView parent,
|
||||||
|
@NonNull RecyclerView.State state) {
|
||||||
|
int position = parent.getChildAdapterPosition(view);
|
||||||
|
int column = position % spanCount;
|
||||||
|
|
||||||
|
outRect.left = spacing - column * spacing / spanCount;
|
||||||
|
outRect.right = (column + 1) * spacing / spanCount;
|
||||||
|
|
||||||
|
if (position >= spanCount) {
|
||||||
|
outRect.top = spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- System UI ----
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (viewerOverlay.getVisibility() == View.VISIBLE) {
|
||||||
|
dismissViewer();
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
824
android/app/src/main/java/com/aethex/os/ProjectsActivity.java
Normal file
824
android/app/src/main/java/com/aethex/os/ProjectsActivity.java
Normal file
|
|
@ -0,0 +1,824 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Projects — Interactive Kanban-style project tracking board.
|
||||||
|
* Supports create, edit, delete, and status cycling.
|
||||||
|
*/
|
||||||
|
public class ProjectsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private Typeface displayFont;
|
||||||
|
private Typeface monoFont;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
|
private LinearLayout projectListContainer;
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "aethex_projects";
|
||||||
|
private static final String KEY_PROJECTS = "projects_json";
|
||||||
|
|
||||||
|
private static final String[] STATUSES = {"Planned", "In Progress", "Review", "Complete"};
|
||||||
|
private static final String[] STATUS_COLORS = {"#66FFFFFF", "#06B6D4", "#FBBF24", "#22C55E"};
|
||||||
|
|
||||||
|
private List<ProjectItem> projects = new ArrayList<>();
|
||||||
|
|
||||||
|
static class ProjectItem {
|
||||||
|
String name;
|
||||||
|
String description;
|
||||||
|
int statusIndex; // 0=Planned, 1=In Progress, 2=Review, 3=Complete
|
||||||
|
|
||||||
|
ProjectItem(String name, String description, int statusIndex) {
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.statusIndex = statusIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
displayFont = themeManager.getDisplayFont(this);
|
||||||
|
monoFont = themeManager.getMonoFont(this);
|
||||||
|
prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
TextView title = findViewById(R.id.app_title);
|
||||||
|
title.setText("Projects");
|
||||||
|
TextView nameDisplay = findViewById(R.id.app_name_display);
|
||||||
|
|
||||||
|
findViewById(R.id.app_back).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout content = (LinearLayout) nameDisplay.getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
content.setGravity(Gravity.TOP);
|
||||||
|
content.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
|
loadProjects();
|
||||||
|
buildProjectsUI(content);
|
||||||
|
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
// Add bottom navigation bar
|
||||||
|
BottomNavBar.attach(this, (ViewGroup) root, BottomNavBar.TAB_PROJECTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildProjectsUI(LinearLayout parent) {
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
|
||||||
|
// Header row with title + add button
|
||||||
|
LinearLayout headerRow = new LinearLayout(this);
|
||||||
|
headerRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
headerRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
headerRow.setPadding(dpToPx(16), dpToPx(14), dpToPx(16), dpToPx(10));
|
||||||
|
|
||||||
|
TextView header = new TextView(this);
|
||||||
|
header.setText("PROJECT BOARD");
|
||||||
|
header.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
header.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
header.setTypeface(monoFont);
|
||||||
|
header.setLetterSpacing(0.15f);
|
||||||
|
header.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
headerRow.addView(header);
|
||||||
|
|
||||||
|
// Project count
|
||||||
|
TextView countView = new TextView(this);
|
||||||
|
countView.setText(projects.size() + " projects");
|
||||||
|
countView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
countView.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
countView.setTypeface(monoFont);
|
||||||
|
countView.setTag("project_count");
|
||||||
|
LinearLayout.LayoutParams cvP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
cvP.setMarginEnd(dpToPx(10));
|
||||||
|
countView.setLayoutParams(cvP);
|
||||||
|
headerRow.addView(countView);
|
||||||
|
|
||||||
|
// Add button
|
||||||
|
TextView addBtn = new TextView(this);
|
||||||
|
addBtn.setText("+ NEW");
|
||||||
|
addBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
addBtn.setTextColor(primaryColor);
|
||||||
|
addBtn.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
addBtn.setLetterSpacing(0.1f);
|
||||||
|
addBtn.setPadding(dpToPx(12), dpToPx(6), dpToPx(12), dpToPx(6));
|
||||||
|
GradientDrawable addBg = new GradientDrawable();
|
||||||
|
addBg.setCornerRadius(dpToPx(6));
|
||||||
|
addBg.setStroke(dpToPx(1), primaryColor);
|
||||||
|
addBg.setColor(Color.parseColor("#0D000000"));
|
||||||
|
addBtn.setBackground(addBg);
|
||||||
|
addBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
showCreateProjectOverlay();
|
||||||
|
});
|
||||||
|
headerRow.addView(addBtn);
|
||||||
|
|
||||||
|
parent.addView(headerRow);
|
||||||
|
|
||||||
|
// Status filter row
|
||||||
|
LinearLayout filterRow = new LinearLayout(this);
|
||||||
|
filterRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
filterRow.setPadding(dpToPx(16), 0, dpToPx(16), dpToPx(10));
|
||||||
|
filterRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
for (int i = 0; i < STATUSES.length; i++) {
|
||||||
|
final int statusIdx = i;
|
||||||
|
int count = 0;
|
||||||
|
for (ProjectItem p : projects) if (p.statusIndex == statusIdx) count++;
|
||||||
|
|
||||||
|
TextView chip = new TextView(this);
|
||||||
|
chip.setText(STATUSES[i] + " " + count);
|
||||||
|
chip.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
chip.setTextColor(Color.parseColor(STATUS_COLORS[i]));
|
||||||
|
chip.setTypeface(monoFont);
|
||||||
|
chip.setPadding(dpToPx(8), dpToPx(4), dpToPx(8), dpToPx(4));
|
||||||
|
GradientDrawable chipBg = new GradientDrawable();
|
||||||
|
chipBg.setCornerRadius(dpToPx(4));
|
||||||
|
chipBg.setColor(Color.parseColor("#0D000000"));
|
||||||
|
chip.setBackground(chipBg);
|
||||||
|
LinearLayout.LayoutParams chipP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
chipP.setMarginEnd(dpToPx(6));
|
||||||
|
chip.setLayoutParams(chipP);
|
||||||
|
filterRow.addView(chip);
|
||||||
|
}
|
||||||
|
parent.addView(filterRow);
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
View div = new View(this);
|
||||||
|
LinearLayout.LayoutParams divP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(1));
|
||||||
|
div.setLayoutParams(divP);
|
||||||
|
div.setBackgroundColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
parent.addView(div);
|
||||||
|
|
||||||
|
// Scrollable project list
|
||||||
|
ScrollView scroll = new ScrollView(this);
|
||||||
|
scroll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
scroll.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
|
||||||
|
projectListContainer = new LinearLayout(this);
|
||||||
|
projectListContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
projectListContainer.setPadding(dpToPx(16), dpToPx(10), dpToPx(16), dpToPx(16));
|
||||||
|
|
||||||
|
refreshProjectList();
|
||||||
|
|
||||||
|
scroll.addView(projectListContainer);
|
||||||
|
parent.addView(scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshProjectList() {
|
||||||
|
projectListContainer.removeAllViews();
|
||||||
|
|
||||||
|
if (projects.isEmpty()) {
|
||||||
|
TextView empty = new TextView(this);
|
||||||
|
empty.setText("No projects yet.\nTap + NEW to create one.");
|
||||||
|
empty.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
empty.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
empty.setTypeface(monoFont);
|
||||||
|
empty.setGravity(Gravity.CENTER);
|
||||||
|
empty.setPadding(0, dpToPx(40), 0, 0);
|
||||||
|
projectListContainer.addView(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < projects.size(); i++) {
|
||||||
|
projectListContainer.addView(buildProjectCard(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update count
|
||||||
|
View root = findViewById(R.id.app_root);
|
||||||
|
if (root != null) {
|
||||||
|
TextView countView = root.findViewWithTag("project_count");
|
||||||
|
if (countView != null) {
|
||||||
|
countView.setText(projects.size() + " projects");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private View buildProjectCard(int index) {
|
||||||
|
ProjectItem project = projects.get(index);
|
||||||
|
int statusColor = Color.parseColor(STATUS_COLORS[project.statusIndex]);
|
||||||
|
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
card.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
cardParams.bottomMargin = dpToPx(8);
|
||||||
|
card.setLayoutParams(cardParams);
|
||||||
|
card.setPadding(dpToPx(16), dpToPx(14), dpToPx(12), dpToPx(14));
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(12));
|
||||||
|
bg.setColor(Color.parseColor("#0D0F172A"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
card.setBackground(bg);
|
||||||
|
|
||||||
|
// Status dot — tap to cycle status
|
||||||
|
View dot = new View(this);
|
||||||
|
int dotSize = dpToPx(10);
|
||||||
|
LinearLayout.LayoutParams dotParams = new LinearLayout.LayoutParams(dotSize, dotSize);
|
||||||
|
dotParams.setMarginEnd(dpToPx(12));
|
||||||
|
dot.setLayoutParams(dotParams);
|
||||||
|
GradientDrawable dotBg = new GradientDrawable();
|
||||||
|
dotBg.setShape(GradientDrawable.OVAL);
|
||||||
|
dotBg.setColor(statusColor);
|
||||||
|
dot.setBackground(dotBg);
|
||||||
|
dot.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
project.statusIndex = (project.statusIndex + 1) % STATUSES.length;
|
||||||
|
saveProjects();
|
||||||
|
// Rebuild entire UI to update filter counts
|
||||||
|
LinearLayout content = (LinearLayout) projectListContainer.getParent().getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
buildProjectsUI(content);
|
||||||
|
AeThexToast.show(this, project.name + " → " + STATUSES[project.statusIndex], AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
card.addView(dot);
|
||||||
|
|
||||||
|
// Info column
|
||||||
|
LinearLayout info = new LinearLayout(this);
|
||||||
|
info.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
info.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
|
||||||
|
TextView name = new TextView(this);
|
||||||
|
name.setText(project.name);
|
||||||
|
name.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
name.setTextColor(Color.parseColor("#CCFFFFFF"));
|
||||||
|
name.setTypeface(displayFont);
|
||||||
|
info.addView(name);
|
||||||
|
|
||||||
|
if (project.description != null && !project.description.isEmpty()) {
|
||||||
|
TextView desc = new TextView(this);
|
||||||
|
desc.setText(project.description);
|
||||||
|
desc.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
desc.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
desc.setTypeface(monoFont);
|
||||||
|
desc.setMaxLines(1);
|
||||||
|
LinearLayout.LayoutParams dP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
dP.topMargin = dpToPx(2);
|
||||||
|
desc.setLayoutParams(dP);
|
||||||
|
info.addView(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView status = new TextView(this);
|
||||||
|
status.setText(STATUSES[project.statusIndex]);
|
||||||
|
status.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
status.setTextColor(statusColor);
|
||||||
|
status.setTypeface(monoFont);
|
||||||
|
LinearLayout.LayoutParams sParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
sParams.topMargin = dpToPx(4);
|
||||||
|
status.setLayoutParams(sParams);
|
||||||
|
info.addView(status);
|
||||||
|
|
||||||
|
card.addView(info);
|
||||||
|
|
||||||
|
// Edit button
|
||||||
|
TextView editBtn = new TextView(this);
|
||||||
|
editBtn.setText("✎");
|
||||||
|
editBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||||
|
editBtn.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
editBtn.setPadding(dpToPx(8), dpToPx(4), dpToPx(8), dpToPx(4));
|
||||||
|
editBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
showEditProjectOverlay(index);
|
||||||
|
});
|
||||||
|
card.addView(editBtn);
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
TextView delBtn = new TextView(this);
|
||||||
|
delBtn.setText("✕");
|
||||||
|
delBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||||
|
delBtn.setTextColor(Color.parseColor("#EF4444"));
|
||||||
|
delBtn.setPadding(dpToPx(8), dpToPx(4), dpToPx(8), dpToPx(4));
|
||||||
|
delBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
showDeleteConfirmation(index);
|
||||||
|
});
|
||||||
|
card.addView(delBtn);
|
||||||
|
|
||||||
|
// Tap card for detail toast
|
||||||
|
card.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
String detail = project.name + "\n" + STATUSES[project.statusIndex];
|
||||||
|
if (project.description != null && !project.description.isEmpty()) {
|
||||||
|
detail += "\n" + project.description;
|
||||||
|
}
|
||||||
|
AeThexToast.show(this, detail, AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCreateProjectOverlay() {
|
||||||
|
showProjectFormOverlay(-1, "", "", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEditProjectOverlay(int index) {
|
||||||
|
ProjectItem p = projects.get(index);
|
||||||
|
showProjectFormOverlay(index, p.name, p.description, p.statusIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showProjectFormOverlay(int editIndex, String currentName, String currentDesc, int currentStatus) {
|
||||||
|
boolean isEdit = editIndex >= 0;
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
FrameLayout root = findViewById(R.id.app_root);
|
||||||
|
|
||||||
|
FrameLayout scrim = new FrameLayout(this);
|
||||||
|
scrim.setTag("project_form_overlay");
|
||||||
|
scrim.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
scrim.setBackgroundColor(Color.parseColor("#CC000000"));
|
||||||
|
|
||||||
|
LinearLayout panel = new LinearLayout(this);
|
||||||
|
panel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
FrameLayout.LayoutParams pp = new FrameLayout.LayoutParams(
|
||||||
|
dpToPx(300), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
pp.gravity = Gravity.CENTER;
|
||||||
|
panel.setLayoutParams(pp);
|
||||||
|
panel.setPadding(dpToPx(20), dpToPx(20), dpToPx(20), dpToPx(20));
|
||||||
|
|
||||||
|
GradientDrawable panelBg = new GradientDrawable();
|
||||||
|
panelBg.setCornerRadius(dpToPx(16));
|
||||||
|
panelBg.setColor(Color.parseColor("#E6111827"));
|
||||||
|
panelBg.setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"));
|
||||||
|
panel.setBackground(panelBg);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
TextView title = new TextView(this);
|
||||||
|
title.setText(isEdit ? "EDIT PROJECT" : "NEW PROJECT");
|
||||||
|
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
title.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
title.setTypeface(monoFont);
|
||||||
|
title.setLetterSpacing(0.15f);
|
||||||
|
LinearLayout.LayoutParams tP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
tP.bottomMargin = dpToPx(16);
|
||||||
|
title.setLayoutParams(tP);
|
||||||
|
panel.addView(title);
|
||||||
|
|
||||||
|
// Name label
|
||||||
|
TextView nameLabel = new TextView(this);
|
||||||
|
nameLabel.setText("PROJECT NAME");
|
||||||
|
nameLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
nameLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
nameLabel.setTypeface(monoFont);
|
||||||
|
panel.addView(nameLabel);
|
||||||
|
|
||||||
|
// Name input
|
||||||
|
EditText nameInput = new EditText(this);
|
||||||
|
nameInput.setText(currentName);
|
||||||
|
nameInput.setTextColor(Color.WHITE);
|
||||||
|
nameInput.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
nameInput.setTypeface(monoFont);
|
||||||
|
nameInput.setSingleLine(true);
|
||||||
|
nameInput.setHint("Enter project name");
|
||||||
|
nameInput.setHintTextColor(Color.parseColor("#33FFFFFF"));
|
||||||
|
nameInput.setPadding(dpToPx(12), dpToPx(10), dpToPx(12), dpToPx(10));
|
||||||
|
nameInput.setShowSoftInputOnFocus(false);
|
||||||
|
GradientDrawable niBg = new GradientDrawable();
|
||||||
|
niBg.setCornerRadius(dpToPx(8));
|
||||||
|
niBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
niBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
nameInput.setBackground(niBg);
|
||||||
|
LinearLayout.LayoutParams niP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
niP.topMargin = dpToPx(4);
|
||||||
|
niP.bottomMargin = dpToPx(14);
|
||||||
|
nameInput.setLayoutParams(niP);
|
||||||
|
nameInput.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus) AeThexKeyboard.attachToEditText(this, nameInput);
|
||||||
|
});
|
||||||
|
panel.addView(nameInput);
|
||||||
|
|
||||||
|
// Description label
|
||||||
|
TextView descLabel = new TextView(this);
|
||||||
|
descLabel.setText("DESCRIPTION");
|
||||||
|
descLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
descLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
descLabel.setTypeface(monoFont);
|
||||||
|
panel.addView(descLabel);
|
||||||
|
|
||||||
|
// Description input
|
||||||
|
EditText descInput = new EditText(this);
|
||||||
|
descInput.setText(currentDesc);
|
||||||
|
descInput.setTextColor(Color.WHITE);
|
||||||
|
descInput.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||||
|
descInput.setTypeface(monoFont);
|
||||||
|
descInput.setMaxLines(3);
|
||||||
|
descInput.setHint("Optional description");
|
||||||
|
descInput.setHintTextColor(Color.parseColor("#33FFFFFF"));
|
||||||
|
descInput.setPadding(dpToPx(12), dpToPx(10), dpToPx(12), dpToPx(10));
|
||||||
|
descInput.setShowSoftInputOnFocus(false);
|
||||||
|
GradientDrawable diBg = new GradientDrawable();
|
||||||
|
diBg.setCornerRadius(dpToPx(8));
|
||||||
|
diBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
diBg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
descInput.setBackground(diBg);
|
||||||
|
LinearLayout.LayoutParams diP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
diP.topMargin = dpToPx(4);
|
||||||
|
diP.bottomMargin = dpToPx(14);
|
||||||
|
descInput.setLayoutParams(diP);
|
||||||
|
descInput.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus) AeThexKeyboard.attachToEditText(this, descInput);
|
||||||
|
});
|
||||||
|
panel.addView(descInput);
|
||||||
|
|
||||||
|
// Status selector (only for edit)
|
||||||
|
final int[] selectedStatus = {currentStatus};
|
||||||
|
if (isEdit) {
|
||||||
|
TextView statusLabel = new TextView(this);
|
||||||
|
statusLabel.setText("STATUS");
|
||||||
|
statusLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
statusLabel.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
statusLabel.setTypeface(monoFont);
|
||||||
|
panel.addView(statusLabel);
|
||||||
|
|
||||||
|
LinearLayout statusRow = new LinearLayout(this);
|
||||||
|
statusRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
LinearLayout.LayoutParams srP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
srP.topMargin = dpToPx(6);
|
||||||
|
srP.bottomMargin = dpToPx(16);
|
||||||
|
statusRow.setLayoutParams(srP);
|
||||||
|
|
||||||
|
TextView[] statusChips = new TextView[STATUSES.length];
|
||||||
|
for (int i = 0; i < STATUSES.length; i++) {
|
||||||
|
final int idx = i;
|
||||||
|
statusChips[i] = new TextView(this);
|
||||||
|
statusChips[i].setText(STATUSES[i]);
|
||||||
|
statusChips[i].setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
statusChips[i].setTypeface(monoFont);
|
||||||
|
statusChips[i].setPadding(dpToPx(8), dpToPx(6), dpToPx(8), dpToPx(6));
|
||||||
|
statusChips[i].setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams scP = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
scP.setMarginEnd(dpToPx(4));
|
||||||
|
statusChips[i].setLayoutParams(scP);
|
||||||
|
|
||||||
|
updateStatusChipStyle(statusChips[i], idx, selectedStatus[0] == idx);
|
||||||
|
|
||||||
|
statusChips[i].setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
selectedStatus[0] = idx;
|
||||||
|
for (int j = 0; j < statusChips.length; j++) {
|
||||||
|
updateStatusChipStyle(statusChips[j], j, j == idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
statusRow.addView(statusChips[i]);
|
||||||
|
}
|
||||||
|
panel.addView(statusRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button row
|
||||||
|
LinearLayout btnRow = new LinearLayout(this);
|
||||||
|
btnRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
btnRow.setGravity(Gravity.END);
|
||||||
|
|
||||||
|
// Cancel
|
||||||
|
TextView cancelBtn = new TextView(this);
|
||||||
|
cancelBtn.setText("CANCEL");
|
||||||
|
cancelBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
cancelBtn.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
cancelBtn.setTypeface(monoFont);
|
||||||
|
cancelBtn.setPadding(dpToPx(16), dpToPx(10), dpToPx(16), dpToPx(10));
|
||||||
|
cancelBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
AeThexKeyboard.dismissKeyboard(this);
|
||||||
|
dismissOverlay("project_form_overlay");
|
||||||
|
});
|
||||||
|
btnRow.addView(cancelBtn);
|
||||||
|
|
||||||
|
// Save
|
||||||
|
TextView saveBtn = new TextView(this);
|
||||||
|
saveBtn.setText(isEdit ? "SAVE" : "CREATE");
|
||||||
|
saveBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
saveBtn.setTextColor(Color.WHITE);
|
||||||
|
saveBtn.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
saveBtn.setGravity(Gravity.CENTER);
|
||||||
|
saveBtn.setPadding(dpToPx(20), dpToPx(10), dpToPx(20), dpToPx(10));
|
||||||
|
GradientDrawable saveBg = new GradientDrawable();
|
||||||
|
saveBg.setCornerRadius(dpToPx(8));
|
||||||
|
saveBg.setColor(primaryColor);
|
||||||
|
saveBtn.setBackground(saveBg);
|
||||||
|
LinearLayout.LayoutParams svP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
svP.setMarginStart(dpToPx(8));
|
||||||
|
saveBtn.setLayoutParams(svP);
|
||||||
|
saveBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
String name = nameInput.getText().toString().trim();
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
AeThexToast.show(this, "Project name required", AeThexToast.Type.INFO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String desc = descInput.getText().toString().trim();
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
ProjectItem p = projects.get(editIndex);
|
||||||
|
p.name = name;
|
||||||
|
p.description = desc;
|
||||||
|
p.statusIndex = selectedStatus[0];
|
||||||
|
} else {
|
||||||
|
projects.add(new ProjectItem(name, desc, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProjects();
|
||||||
|
AeThexKeyboard.dismissKeyboard(this);
|
||||||
|
dismissOverlay("project_form_overlay");
|
||||||
|
|
||||||
|
// Rebuild UI
|
||||||
|
LinearLayout content = (LinearLayout) projectListContainer.getParent().getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
buildProjectsUI(content);
|
||||||
|
|
||||||
|
AeThexToast.show(this,
|
||||||
|
isEdit ? "Project updated" : "Project created",
|
||||||
|
AeThexToast.Type.SUCCESS);
|
||||||
|
});
|
||||||
|
btnRow.addView(saveBtn);
|
||||||
|
|
||||||
|
panel.addView(btnRow);
|
||||||
|
scrim.addView(panel);
|
||||||
|
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
root.addView(scrim);
|
||||||
|
scrim.animate().alpha(1f).setDuration(200).start();
|
||||||
|
|
||||||
|
nameInput.requestFocus();
|
||||||
|
nameInput.postDelayed(() -> AeThexKeyboard.attachToEditText(this, nameInput), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatusChipStyle(TextView chip, int statusIdx, boolean selected) {
|
||||||
|
int color = Color.parseColor(STATUS_COLORS[statusIdx]);
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(6));
|
||||||
|
if (selected) {
|
||||||
|
bg.setColor(Color.argb(40, Color.red(color), Color.green(color), Color.blue(color)));
|
||||||
|
bg.setStroke(dpToPx(1), color);
|
||||||
|
chip.setTextColor(color);
|
||||||
|
} else {
|
||||||
|
bg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#1AFFFFFF"));
|
||||||
|
chip.setTextColor(Color.parseColor("#4DFFFFFF"));
|
||||||
|
}
|
||||||
|
chip.setBackground(bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDeleteConfirmation(int index) {
|
||||||
|
int primaryColor = themeManager.getPrimaryColor(this);
|
||||||
|
FrameLayout root = findViewById(R.id.app_root);
|
||||||
|
|
||||||
|
FrameLayout scrim = new FrameLayout(this);
|
||||||
|
scrim.setTag("project_delete_overlay");
|
||||||
|
scrim.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
scrim.setBackgroundColor(Color.parseColor("#CC000000"));
|
||||||
|
|
||||||
|
LinearLayout panel = new LinearLayout(this);
|
||||||
|
panel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
panel.setGravity(Gravity.CENTER);
|
||||||
|
FrameLayout.LayoutParams pp = new FrameLayout.LayoutParams(
|
||||||
|
dpToPx(260), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
pp.gravity = Gravity.CENTER;
|
||||||
|
panel.setLayoutParams(pp);
|
||||||
|
panel.setPadding(dpToPx(20), dpToPx(20), dpToPx(20), dpToPx(20));
|
||||||
|
|
||||||
|
GradientDrawable panelBg = new GradientDrawable();
|
||||||
|
panelBg.setCornerRadius(dpToPx(16));
|
||||||
|
panelBg.setColor(Color.parseColor("#E6111827"));
|
||||||
|
panelBg.setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"));
|
||||||
|
panel.setBackground(panelBg);
|
||||||
|
|
||||||
|
// Warning icon
|
||||||
|
TextView icon = new TextView(this);
|
||||||
|
icon.setText("⚠");
|
||||||
|
icon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 28);
|
||||||
|
icon.setGravity(Gravity.CENTER);
|
||||||
|
panel.addView(icon);
|
||||||
|
|
||||||
|
// Message
|
||||||
|
TextView msg = new TextView(this);
|
||||||
|
msg.setText("Delete \"" + projects.get(index).name + "\"?");
|
||||||
|
msg.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
msg.setTextColor(Color.WHITE);
|
||||||
|
msg.setTypeface(displayFont);
|
||||||
|
msg.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams mP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
mP.topMargin = dpToPx(10);
|
||||||
|
mP.bottomMargin = dpToPx(6);
|
||||||
|
msg.setLayoutParams(mP);
|
||||||
|
panel.addView(msg);
|
||||||
|
|
||||||
|
TextView sub = new TextView(this);
|
||||||
|
sub.setText("This action cannot be undone.");
|
||||||
|
sub.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||||
|
sub.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
sub.setTypeface(monoFont);
|
||||||
|
sub.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams sP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
sP.bottomMargin = dpToPx(16);
|
||||||
|
sub.setLayoutParams(sP);
|
||||||
|
panel.addView(sub);
|
||||||
|
|
||||||
|
// Button row
|
||||||
|
LinearLayout btnRow = new LinearLayout(this);
|
||||||
|
btnRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
btnRow.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
TextView cancelBtn = new TextView(this);
|
||||||
|
cancelBtn.setText("CANCEL");
|
||||||
|
cancelBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
cancelBtn.setTextColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
cancelBtn.setTypeface(monoFont);
|
||||||
|
cancelBtn.setPadding(dpToPx(20), dpToPx(10), dpToPx(20), dpToPx(10));
|
||||||
|
cancelBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
dismissOverlay("project_delete_overlay");
|
||||||
|
});
|
||||||
|
btnRow.addView(cancelBtn);
|
||||||
|
|
||||||
|
TextView delBtn = new TextView(this);
|
||||||
|
delBtn.setText("DELETE");
|
||||||
|
delBtn.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
delBtn.setTextColor(Color.WHITE);
|
||||||
|
delBtn.setTypeface(monoFont, Typeface.BOLD);
|
||||||
|
delBtn.setGravity(Gravity.CENTER);
|
||||||
|
delBtn.setPadding(dpToPx(20), dpToPx(10), dpToPx(20), dpToPx(10));
|
||||||
|
GradientDrawable delBg = new GradientDrawable();
|
||||||
|
delBg.setCornerRadius(dpToPx(8));
|
||||||
|
delBg.setColor(Color.parseColor("#EF4444"));
|
||||||
|
delBtn.setBackground(delBg);
|
||||||
|
LinearLayout.LayoutParams dbP = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
dbP.setMarginStart(dpToPx(10));
|
||||||
|
delBtn.setLayoutParams(dbP);
|
||||||
|
delBtn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
String removedName = projects.get(index).name;
|
||||||
|
projects.remove(index);
|
||||||
|
saveProjects();
|
||||||
|
dismissOverlay("project_delete_overlay");
|
||||||
|
|
||||||
|
// Rebuild UI
|
||||||
|
LinearLayout content = (LinearLayout) projectListContainer.getParent().getParent();
|
||||||
|
content.removeAllViews();
|
||||||
|
buildProjectsUI(content);
|
||||||
|
|
||||||
|
AeThexToast.show(this, removedName + " deleted", AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
btnRow.addView(delBtn);
|
||||||
|
|
||||||
|
panel.addView(btnRow);
|
||||||
|
scrim.addView(panel);
|
||||||
|
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
root.addView(scrim);
|
||||||
|
scrim.animate().alpha(1f).setDuration(200).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissOverlay(String tag) {
|
||||||
|
FrameLayout root = findViewById(R.id.app_root);
|
||||||
|
View overlay = root.findViewWithTag(tag);
|
||||||
|
if (overlay != null) {
|
||||||
|
overlay.animate().alpha(0f).setDuration(150).withEndAction(() -> {
|
||||||
|
root.removeView(overlay);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Persistence ──
|
||||||
|
|
||||||
|
private void loadProjects() {
|
||||||
|
projects.clear();
|
||||||
|
String json = prefs.getString(KEY_PROJECTS, null);
|
||||||
|
if (json != null) {
|
||||||
|
try {
|
||||||
|
JSONArray arr = new JSONArray(json);
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONObject obj = arr.getJSONObject(i);
|
||||||
|
projects.add(new ProjectItem(
|
||||||
|
obj.getString("name"),
|
||||||
|
obj.optString("desc", ""),
|
||||||
|
obj.getInt("status")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed default projects if none exist
|
||||||
|
if (projects.isEmpty()) {
|
||||||
|
projects.add(new ProjectItem("AeThex Mobile", "Android native client", 1));
|
||||||
|
projects.add(new ProjectItem("Security Audit", "Q1 security review", 2));
|
||||||
|
projects.add(new ProjectItem("Data Pipeline", "Streaming analytics pipeline", 3));
|
||||||
|
projects.add(new ProjectItem("UI Redesign", "Glass morphism theme system", 1));
|
||||||
|
projects.add(new ProjectItem("API Gateway", "REST & WebSocket endpoints", 0));
|
||||||
|
saveProjects();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveProjects() {
|
||||||
|
try {
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
for (ProjectItem p : projects) {
|
||||||
|
JSONObject obj = new JSONObject();
|
||||||
|
obj.put("name", p.name);
|
||||||
|
obj.put("desc", p.description);
|
||||||
|
obj.put("status", p.statusIndex);
|
||||||
|
arr.put(obj);
|
||||||
|
}
|
||||||
|
prefs.edit().putString(KEY_PROJECTS, arr.toString()).apply();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
FrameLayout root = findViewById(R.id.app_root);
|
||||||
|
// Check for overlays
|
||||||
|
View formOverlay = root.findViewWithTag("project_form_overlay");
|
||||||
|
if (formOverlay != null) {
|
||||||
|
AeThexKeyboard.dismissKeyboard(this);
|
||||||
|
dismissOverlay("project_form_overlay");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
View delOverlay = root.findViewWithTag("project_delete_overlay");
|
||||||
|
if (delOverlay != null) {
|
||||||
|
dismissOverlay("project_delete_overlay");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
super.onBackPressed();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
253
android/app/src/main/java/com/aethex/os/RootShell.java
Normal file
253
android/app/src/main/java/com/aethex/os/RootShell.java
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RootShell - Execute commands with root privileges on AeThexOS.
|
||||||
|
* Provides both synchronous and asynchronous execution methods.
|
||||||
|
*/
|
||||||
|
public class RootShell {
|
||||||
|
|
||||||
|
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
private static Boolean rootAvailable = null;
|
||||||
|
|
||||||
|
public interface CommandCallback {
|
||||||
|
void onOutput(String output);
|
||||||
|
void onError(String error);
|
||||||
|
void onComplete(int exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if root access is available
|
||||||
|
*/
|
||||||
|
public static boolean isRootAvailable() {
|
||||||
|
if (rootAvailable != null) {
|
||||||
|
return rootAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Process process = Runtime.getRuntime().exec("su -c id");
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
String line = reader.readLine();
|
||||||
|
process.waitFor();
|
||||||
|
reader.close();
|
||||||
|
|
||||||
|
rootAvailable = (line != null && line.contains("uid=0"));
|
||||||
|
return rootAvailable;
|
||||||
|
} catch (Exception e) {
|
||||||
|
rootAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command as root synchronously
|
||||||
|
* @return Command output or error message
|
||||||
|
*/
|
||||||
|
public static String execute(String command) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
StringBuilder error = new StringBuilder();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Process process = Runtime.getRuntime().exec("su");
|
||||||
|
DataOutputStream os = new DataOutputStream(process.getOutputStream());
|
||||||
|
BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
||||||
|
|
||||||
|
os.writeBytes(command + "\n");
|
||||||
|
os.writeBytes("exit\n");
|
||||||
|
os.flush();
|
||||||
|
|
||||||
|
String line;
|
||||||
|
while ((line = stdout.readLine()) != null) {
|
||||||
|
output.append(line).append("\n");
|
||||||
|
}
|
||||||
|
while ((line = stderr.readLine()) != null) {
|
||||||
|
error.append(line).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
process.waitFor();
|
||||||
|
os.close();
|
||||||
|
stdout.close();
|
||||||
|
stderr.close();
|
||||||
|
|
||||||
|
if (error.length() > 0 && output.length() == 0) {
|
||||||
|
return "Error: " + error.toString().trim();
|
||||||
|
}
|
||||||
|
return output.toString().trim();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command without root (normal shell)
|
||||||
|
*/
|
||||||
|
public static String executeNonRoot(String command) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", command});
|
||||||
|
BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
||||||
|
|
||||||
|
String line;
|
||||||
|
while ((line = stdout.readLine()) != null) {
|
||||||
|
output.append(line).append("\n");
|
||||||
|
}
|
||||||
|
while ((line = stderr.readLine()) != null) {
|
||||||
|
output.append(line).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
process.waitFor();
|
||||||
|
stdout.close();
|
||||||
|
stderr.close();
|
||||||
|
|
||||||
|
return output.toString().trim();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command asynchronously with callbacks
|
||||||
|
*/
|
||||||
|
public static void executeAsync(String command, boolean useRoot, CommandCallback callback) {
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
Process process;
|
||||||
|
DataOutputStream os = null;
|
||||||
|
|
||||||
|
if (useRoot) {
|
||||||
|
process = Runtime.getRuntime().exec("su");
|
||||||
|
os = new DataOutputStream(process.getOutputStream());
|
||||||
|
os.writeBytes(command + "\n");
|
||||||
|
os.writeBytes("exit\n");
|
||||||
|
os.flush();
|
||||||
|
} else {
|
||||||
|
process = Runtime.getRuntime().exec(new String[]{"sh", "-c", command});
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
||||||
|
|
||||||
|
// Read output line by line and post to main thread
|
||||||
|
String line;
|
||||||
|
while ((line = stdout.readLine()) != null) {
|
||||||
|
final String outputLine = line;
|
||||||
|
mainHandler.post(() -> callback.onOutput(outputLine));
|
||||||
|
}
|
||||||
|
while ((line = stderr.readLine()) != null) {
|
||||||
|
final String errorLine = line;
|
||||||
|
mainHandler.post(() -> callback.onError(errorLine));
|
||||||
|
}
|
||||||
|
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
if (os != null) os.close();
|
||||||
|
stdout.close();
|
||||||
|
stderr.close();
|
||||||
|
|
||||||
|
mainHandler.post(() -> callback.onComplete(exitCode));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
mainHandler.post(() -> callback.onError("Exception: " + e.getMessage()));
|
||||||
|
mainHandler.post(() -> callback.onComplete(-1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device CPU info from /proc/cpuinfo
|
||||||
|
*/
|
||||||
|
public static String getCpuInfo() {
|
||||||
|
return executeNonRoot("cat /proc/cpuinfo | head -20");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get memory info
|
||||||
|
*/
|
||||||
|
public static String getMemInfo() {
|
||||||
|
return executeNonRoot("cat /proc/meminfo | head -10");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get kernel version
|
||||||
|
*/
|
||||||
|
public static String getKernelVersion() {
|
||||||
|
return executeNonRoot("uname -a");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of running processes (root)
|
||||||
|
*/
|
||||||
|
public static String getProcessList() {
|
||||||
|
if (isRootAvailable()) {
|
||||||
|
return execute("ps -A | head -30");
|
||||||
|
}
|
||||||
|
return executeNonRoot("ps | head -20");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get SELinux status
|
||||||
|
*/
|
||||||
|
public static String getSelinuxStatus() {
|
||||||
|
return executeNonRoot("getenforce 2>/dev/null || echo 'Unknown'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mounted filesystems
|
||||||
|
*/
|
||||||
|
public static String getMounts() {
|
||||||
|
return executeNonRoot("mount | head -15");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get network interfaces
|
||||||
|
*/
|
||||||
|
public static String getNetworkInterfaces() {
|
||||||
|
return executeNonRoot("ip addr 2>/dev/null || ifconfig 2>/dev/null || echo 'Network info unavailable'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get disk usage
|
||||||
|
*/
|
||||||
|
public static String getDiskUsage() {
|
||||||
|
return executeNonRoot("df -h 2>/dev/null || df 2>/dev/null");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remount system as read-write (requires root)
|
||||||
|
*/
|
||||||
|
public static String remountSystemRW() {
|
||||||
|
if (!isRootAvailable()) {
|
||||||
|
return "Error: Root access required";
|
||||||
|
}
|
||||||
|
return execute("mount -o rw,remount /system 2>&1 || mount -o rw,remount / 2>&1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get prop value
|
||||||
|
*/
|
||||||
|
public static String getProp(String prop) {
|
||||||
|
return executeNonRoot("getprop " + prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set prop value (requires root)
|
||||||
|
*/
|
||||||
|
public static String setProp(String prop, String value) {
|
||||||
|
if (!isRootAvailable()) {
|
||||||
|
return "Error: Root access required";
|
||||||
|
}
|
||||||
|
return execute("setprop " + prop + " " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
164
android/app/src/main/java/com/aethex/os/ScreenTimeTracker.java
Normal file
164
android/app/src/main/java/com/aethex/os/ScreenTimeTracker.java
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks app launch counts and screen time per app.
|
||||||
|
* Used for auto-categorization and analytics.
|
||||||
|
*/
|
||||||
|
public class ScreenTimeTracker {
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "aethex_screen_time";
|
||||||
|
private static final String KEY_LAUNCH_COUNTS = "launch_counts";
|
||||||
|
private static final String KEY_PINNED_APPS = "pinned_apps";
|
||||||
|
private static final String KEY_HIDDEN_APPS = "hidden_apps";
|
||||||
|
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
|
||||||
|
public ScreenTimeTracker(Context context) {
|
||||||
|
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record an app launch.
|
||||||
|
*/
|
||||||
|
public void recordLaunch(String appId) {
|
||||||
|
try {
|
||||||
|
String json = prefs.getString(KEY_LAUNCH_COUNTS, "{}");
|
||||||
|
JSONObject counts = new JSONObject(json);
|
||||||
|
int current = counts.optInt(appId, 0);
|
||||||
|
counts.put(appId, current + 1);
|
||||||
|
prefs.edit().putString(KEY_LAUNCH_COUNTS, counts.toString()).apply();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get launch count for an app.
|
||||||
|
*/
|
||||||
|
public int getLaunchCount(String appId) {
|
||||||
|
try {
|
||||||
|
String json = prefs.getString(KEY_LAUNCH_COUNTS, "{}");
|
||||||
|
JSONObject counts = new JSONObject(json);
|
||||||
|
return counts.optInt(appId, 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total launches across all apps.
|
||||||
|
*/
|
||||||
|
public int getTotalLaunches() {
|
||||||
|
try {
|
||||||
|
String json = prefs.getString(KEY_LAUNCH_COUNTS, "{}");
|
||||||
|
JSONObject counts = new JSONObject(json);
|
||||||
|
int total = 0;
|
||||||
|
Iterator<String> keys = counts.keys();
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
total += counts.optInt(keys.next(), 0);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the top N most used app IDs.
|
||||||
|
*/
|
||||||
|
public List<String> getMostUsed(int limit) {
|
||||||
|
try {
|
||||||
|
String json = prefs.getString(KEY_LAUNCH_COUNTS, "{}");
|
||||||
|
JSONObject counts = new JSONObject(json);
|
||||||
|
List<String[]> entries = new ArrayList<>();
|
||||||
|
Iterator<String> keys = counts.keys();
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
String key = keys.next();
|
||||||
|
entries.add(new String[]{key, String.valueOf(counts.optInt(key, 0))});
|
||||||
|
}
|
||||||
|
Collections.sort(entries, (a, b) -> Integer.parseInt(b[1]) - Integer.parseInt(a[1]));
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
for (int i = 0; i < Math.min(limit, entries.size()); i++) {
|
||||||
|
result.add(entries.get(i)[0]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize an app as: "pinned", "most_used", "essential", or "other".
|
||||||
|
*/
|
||||||
|
public String categorize(String appId) {
|
||||||
|
if (isPinned(appId)) return "pinned";
|
||||||
|
int count = getLaunchCount(appId);
|
||||||
|
if (count >= 10) return "most_used";
|
||||||
|
// Essential = built-in AeThexOS core apps
|
||||||
|
if (isEssential(appId)) return "essential";
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEssential(String appId) {
|
||||||
|
switch (appId) {
|
||||||
|
case "terminal":
|
||||||
|
case "browser":
|
||||||
|
case "settings":
|
||||||
|
case "files":
|
||||||
|
case "notes":
|
||||||
|
case "chat":
|
||||||
|
case "mail":
|
||||||
|
case "camera":
|
||||||
|
case "photos":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pinning ──
|
||||||
|
|
||||||
|
public boolean isPinned(String appId) {
|
||||||
|
return prefs.getStringSet(KEY_PINNED_APPS, Collections.emptySet()).contains(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPinned(String appId, boolean pinned) {
|
||||||
|
java.util.Set<String> set = new java.util.HashSet<>(
|
||||||
|
prefs.getStringSet(KEY_PINNED_APPS, Collections.emptySet()));
|
||||||
|
if (pinned) set.add(appId);
|
||||||
|
else set.remove(appId);
|
||||||
|
prefs.edit().putStringSet(KEY_PINNED_APPS, set).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hiding ──
|
||||||
|
|
||||||
|
public boolean isHidden(String appId) {
|
||||||
|
return prefs.getStringSet(KEY_HIDDEN_APPS, Collections.emptySet()).contains(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHidden(String appId, boolean hidden) {
|
||||||
|
java.util.Set<String> set = new java.util.HashSet<>(
|
||||||
|
prefs.getStringSet(KEY_HIDDEN_APPS, Collections.emptySet()));
|
||||||
|
if (hidden) set.add(appId);
|
||||||
|
else set.remove(appId);
|
||||||
|
prefs.edit().putStringSet(KEY_HIDDEN_APPS, set).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all hidden app IDs.
|
||||||
|
*/
|
||||||
|
public java.util.Set<String> getHiddenApps() {
|
||||||
|
return prefs.getStringSet(KEY_HIDDEN_APPS, Collections.emptySet());
|
||||||
|
}
|
||||||
|
}
|
||||||
537
android/app/src/main/java/com/aethex/os/SettingsActivity.java
Normal file
537
android/app/src/main/java/com/aethex/os/SettingsActivity.java
Normal file
|
|
@ -0,0 +1,537 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class SettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private TextView currentModeLabel;
|
||||||
|
private LinearLayout accentColorRow;
|
||||||
|
private LinearLayout wallpaperGrid;
|
||||||
|
private SeekBar transparencySlider;
|
||||||
|
private TextView transparencyValue;
|
||||||
|
private TextView soundToggle;
|
||||||
|
private TextView accentReset;
|
||||||
|
private LinearLayout aboutCard;
|
||||||
|
private TextView aboutKonamiHint;
|
||||||
|
|
||||||
|
private int konamiTapCount = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Sync SoundManager with persisted setting
|
||||||
|
SoundManager.getInstance().setEnabled(themeManager.isSoundEnabled());
|
||||||
|
|
||||||
|
// Bind views
|
||||||
|
currentModeLabel = findViewById(R.id.current_mode_label);
|
||||||
|
accentColorRow = findViewById(R.id.accent_color_row);
|
||||||
|
wallpaperGrid = findViewById(R.id.wallpaper_grid);
|
||||||
|
transparencySlider = findViewById(R.id.transparency_slider);
|
||||||
|
transparencyValue = findViewById(R.id.transparency_value);
|
||||||
|
soundToggle = findViewById(R.id.sound_toggle);
|
||||||
|
accentReset = findViewById(R.id.accent_reset);
|
||||||
|
aboutCard = findViewById(R.id.about_card);
|
||||||
|
aboutKonamiHint = findViewById(R.id.about_konami_hint);
|
||||||
|
|
||||||
|
// ── Back button ──
|
||||||
|
findViewById(R.id.settings_back).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Clearance mode buttons ──
|
||||||
|
setupClearanceButtons();
|
||||||
|
|
||||||
|
// ── Accent colors ──
|
||||||
|
setupAccentColors();
|
||||||
|
|
||||||
|
// ── Wallpaper picker ──
|
||||||
|
setupWallpaperPicker();
|
||||||
|
|
||||||
|
// ── Transparency slider ──
|
||||||
|
setupTransparencySlider();
|
||||||
|
|
||||||
|
// ── Sound toggle ──
|
||||||
|
setupSoundToggle();
|
||||||
|
|
||||||
|
// ── Device Settings & Hidden Apps ──
|
||||||
|
setupDeviceSettings();
|
||||||
|
setupHiddenApps();
|
||||||
|
|
||||||
|
// ── Konami easter egg ──
|
||||||
|
setupKonamiEasterEgg();
|
||||||
|
|
||||||
|
// Update all display states
|
||||||
|
updateModeLabel();
|
||||||
|
highlightSelection();
|
||||||
|
updateSoundToggle();
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View content = findViewById(R.id.settings_root);
|
||||||
|
content.setAlpha(0f);
|
||||||
|
content.animate().alpha(1f).setDuration(300).start();
|
||||||
|
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
// Add bottom navigation bar
|
||||||
|
BottomNavBar.attach(this, (ViewGroup) content, BottomNavBar.TAB_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Clearance Mode
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupClearanceButtons() {
|
||||||
|
LinearLayout btnFoundation = findViewById(R.id.btn_foundation);
|
||||||
|
LinearLayout btnCorp = findViewById(R.id.btn_corp);
|
||||||
|
|
||||||
|
btnFoundation.setOnClickListener(v -> {
|
||||||
|
if (themeManager.isFoundation()) return; // Already selected
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
v.animate().scaleX(0.95f).scaleY(0.95f).setDuration(80).withEndAction(() -> {
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(80).start();
|
||||||
|
switchClearance(ThemeManager.CLEARANCE_FOUNDATION);
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCorp.setOnClickListener(v -> {
|
||||||
|
if (!themeManager.isFoundation()) return; // Already selected
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
v.animate().scaleX(0.95f).scaleY(0.95f).setDuration(80).withEndAction(() -> {
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(80).start();
|
||||||
|
switchClearance(ThemeManager.CLEARANCE_CORP);
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void switchClearance(String targetMode) {
|
||||||
|
ClearanceSwitchOverlay.show(this, targetMode, () -> {
|
||||||
|
themeManager.setClearanceMode(targetMode);
|
||||||
|
updateModeLabel();
|
||||||
|
highlightSelection();
|
||||||
|
refreshWallpaperPicker();
|
||||||
|
AeThexToast.show(this,
|
||||||
|
ThemeManager.CLEARANCE_FOUNDATION.equals(targetMode)
|
||||||
|
? "Foundation clearance activated"
|
||||||
|
: "Corp clearance activated",
|
||||||
|
AeThexToast.Type.SUCCESS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateModeLabel() {
|
||||||
|
String mode = themeManager.isFoundation() ? "Foundation" : "Corp";
|
||||||
|
currentModeLabel.setText("Current: " + mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void highlightSelection() {
|
||||||
|
LinearLayout btnFoundation = findViewById(R.id.btn_foundation);
|
||||||
|
LinearLayout btnCorp = findViewById(R.id.btn_corp);
|
||||||
|
|
||||||
|
float density = getResources().getDisplayMetrics().density;
|
||||||
|
int cornerRadius = (int) (16 * density);
|
||||||
|
|
||||||
|
if (themeManager.isFoundation()) {
|
||||||
|
GradientDrawable foundBg = new GradientDrawable();
|
||||||
|
foundBg.setColor(0x33DC2626);
|
||||||
|
foundBg.setCornerRadius(cornerRadius);
|
||||||
|
foundBg.setStroke((int) (2 * density), 0xFFDC2626);
|
||||||
|
btnFoundation.setBackground(foundBg);
|
||||||
|
|
||||||
|
GradientDrawable corpBg = new GradientDrawable();
|
||||||
|
corpBg.setColor(0x1AFFFFFF);
|
||||||
|
corpBg.setCornerRadius(cornerRadius);
|
||||||
|
corpBg.setStroke((int) (1 * density), 0x1AFFFFFF);
|
||||||
|
btnCorp.setBackground(corpBg);
|
||||||
|
} else {
|
||||||
|
GradientDrawable foundBg = new GradientDrawable();
|
||||||
|
foundBg.setColor(0x1AFFFFFF);
|
||||||
|
foundBg.setCornerRadius(cornerRadius);
|
||||||
|
foundBg.setStroke((int) (1 * density), 0x1AFFFFFF);
|
||||||
|
btnFoundation.setBackground(foundBg);
|
||||||
|
|
||||||
|
GradientDrawable corpBg = new GradientDrawable();
|
||||||
|
corpBg.setColor(0x332563EB);
|
||||||
|
corpBg.setCornerRadius(cornerRadius);
|
||||||
|
corpBg.setStroke((int) (2 * density), 0xFF2563EB);
|
||||||
|
btnCorp.setBackground(corpBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Accent Colors
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupAccentColors() {
|
||||||
|
accentColorRow.removeAllViews();
|
||||||
|
String currentAccent = themeManager.getCustomAccentColorHex();
|
||||||
|
|
||||||
|
for (String[] accentDef : ThemeManager.ACCENT_COLORS) {
|
||||||
|
String name = accentDef[0];
|
||||||
|
String hex = accentDef[1];
|
||||||
|
|
||||||
|
FrameLayout swatch = new FrameLayout(this);
|
||||||
|
int size = dpToPx(36);
|
||||||
|
LinearLayout.LayoutParams swatchParams = new LinearLayout.LayoutParams(size, size);
|
||||||
|
swatchParams.setMarginEnd(dpToPx(10));
|
||||||
|
swatch.setLayoutParams(swatchParams);
|
||||||
|
|
||||||
|
boolean isSelected = hex.equalsIgnoreCase(currentAccent);
|
||||||
|
|
||||||
|
GradientDrawable swatchBg = new GradientDrawable();
|
||||||
|
swatchBg.setShape(GradientDrawable.OVAL);
|
||||||
|
swatchBg.setColor(Color.parseColor(hex));
|
||||||
|
if (isSelected) {
|
||||||
|
swatchBg.setStroke(dpToPx(2), Color.WHITE);
|
||||||
|
}
|
||||||
|
swatch.setBackground(swatchBg);
|
||||||
|
|
||||||
|
// Checkmark for selected
|
||||||
|
if (isSelected) {
|
||||||
|
TextView check = new TextView(this);
|
||||||
|
check.setText("✓");
|
||||||
|
check.setTextColor(Color.WHITE);
|
||||||
|
check.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
check.setGravity(Gravity.CENTER);
|
||||||
|
swatch.addView(check, new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
swatch.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
themeManager.setCustomAccentColor(hex);
|
||||||
|
setupAccentColors(); // Rebuild to update selection
|
||||||
|
AeThexToast.show(this, "Accent: " + name, AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
accentColorRow.addView(swatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
accentReset.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
themeManager.setCustomAccentColor(null);
|
||||||
|
setupAccentColors();
|
||||||
|
AeThexToast.show(this, "Accent reset to default", AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Wallpaper Picker
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupWallpaperPicker() {
|
||||||
|
refreshWallpaperPicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshWallpaperPicker() {
|
||||||
|
wallpaperGrid.removeAllViews();
|
||||||
|
String currentWallpaper = themeManager.getWallpaperId();
|
||||||
|
boolean konamiUnlocked = themeManager.isKonamiUnlocked();
|
||||||
|
Typeface monoFont = themeManager.getMonoFont(this);
|
||||||
|
|
||||||
|
// Build rows of 2 wallpaper cards
|
||||||
|
LinearLayout currentRow = null;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (String[] wallDef : ThemeManager.WALLPAPERS) {
|
||||||
|
String id = wallDef[0];
|
||||||
|
String name = wallDef[1];
|
||||||
|
boolean isSecret = "true".equals(wallDef[2]);
|
||||||
|
|
||||||
|
// Skip secret wallpapers if not unlocked
|
||||||
|
if (isSecret && !konamiUnlocked) continue;
|
||||||
|
|
||||||
|
if (count % 2 == 0) {
|
||||||
|
currentRow = new LinearLayout(this);
|
||||||
|
currentRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
rowParams.bottomMargin = dpToPx(10);
|
||||||
|
currentRow.setLayoutParams(rowParams);
|
||||||
|
wallpaperGrid.addView(currentRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSelected = id.equals(currentWallpaper);
|
||||||
|
View card = createWallpaperCard(id, name, isSelected, isSecret, monoFont);
|
||||||
|
currentRow.addView(card);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createWallpaperCard(String id, String name, boolean isSelected,
|
||||||
|
boolean isSecret, Typeface monoFont) {
|
||||||
|
LinearLayout card = new LinearLayout(this);
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
card.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
cardParams.setMarginEnd(dpToPx(8));
|
||||||
|
card.setLayoutParams(cardParams);
|
||||||
|
card.setPadding(dpToPx(8), dpToPx(8), dpToPx(8), dpToPx(8));
|
||||||
|
|
||||||
|
// Card background
|
||||||
|
GradientDrawable cardBg = new GradientDrawable();
|
||||||
|
cardBg.setCornerRadius(dpToPx(12));
|
||||||
|
if (isSelected) {
|
||||||
|
cardBg.setStroke(dpToPx(2), Color.WHITE);
|
||||||
|
cardBg.setColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
} else {
|
||||||
|
cardBg.setStroke(dpToPx(1), Color.parseColor("#33FFFFFF"));
|
||||||
|
cardBg.setColor(Color.parseColor("#0DFFFFFF"));
|
||||||
|
}
|
||||||
|
card.setBackground(cardBg);
|
||||||
|
|
||||||
|
// Preview gradient
|
||||||
|
View preview = new View(this);
|
||||||
|
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(60));
|
||||||
|
preview.setLayoutParams(previewParams);
|
||||||
|
|
||||||
|
GradientDrawable previewGd;
|
||||||
|
if (ThemeManager.WALLPAPER_DEFAULT.equals(id)) {
|
||||||
|
// Show current theme default wallpaper
|
||||||
|
previewGd = themeManager.createWallpaperDrawable(this);
|
||||||
|
} else {
|
||||||
|
previewGd = ThemeManager.createWallpaperPreview(id);
|
||||||
|
}
|
||||||
|
previewGd.setCornerRadius(dpToPx(8));
|
||||||
|
preview.setBackground(previewGd);
|
||||||
|
card.addView(preview);
|
||||||
|
|
||||||
|
// Name label
|
||||||
|
TextView label = new TextView(this);
|
||||||
|
label.setText(name);
|
||||||
|
label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
|
||||||
|
label.setTextColor(isSelected ? Color.WHITE : Color.parseColor("#99FFFFFF"));
|
||||||
|
label.setTypeface(monoFont);
|
||||||
|
label.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
labelParams.topMargin = dpToPx(6);
|
||||||
|
label.setLayoutParams(labelParams);
|
||||||
|
card.addView(label);
|
||||||
|
|
||||||
|
// Selected indicator
|
||||||
|
if (isSelected) {
|
||||||
|
TextView check = new TextView(this);
|
||||||
|
check.setText("● Active");
|
||||||
|
check.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9);
|
||||||
|
check.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
check.setTypeface(monoFont);
|
||||||
|
check.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams checkParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
checkParams.topMargin = dpToPx(2);
|
||||||
|
check.setLayoutParams(checkParams);
|
||||||
|
card.addView(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
themeManager.setWallpaperId(id);
|
||||||
|
refreshWallpaperPicker();
|
||||||
|
AeThexToast.show(this, "Wallpaper: " + name, AeThexToast.Type.SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Transparency Slider
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupTransparencySlider() {
|
||||||
|
int currentTransparency = themeManager.getTransparency();
|
||||||
|
// SeekBar max=50, representing 50-100 range
|
||||||
|
transparencySlider.setProgress(currentTransparency - 50);
|
||||||
|
transparencyValue.setText("Current: " + currentTransparency + "%");
|
||||||
|
|
||||||
|
transparencySlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
int value = progress + 50; // Map 0-50 to 50-100
|
||||||
|
transparencyValue.setText("Current: " + value + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
|
int value = seekBar.getProgress() + 50;
|
||||||
|
themeManager.setTransparency(value);
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
AeThexToast.show(SettingsActivity.this,
|
||||||
|
"Panel transparency: " + value + "%", AeThexToast.Type.INFO);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Sound Toggle
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupSoundToggle() {
|
||||||
|
soundToggle.setOnClickListener(v -> {
|
||||||
|
boolean newState = !themeManager.isSoundEnabled();
|
||||||
|
themeManager.setSoundEnabled(newState);
|
||||||
|
SoundManager.getInstance().setEnabled(newState);
|
||||||
|
updateSoundToggle();
|
||||||
|
if (newState) {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.NOTIFICATION);
|
||||||
|
}
|
||||||
|
AeThexToast.show(this, "Sound " + (newState ? "enabled" : "disabled"),
|
||||||
|
AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSoundToggle() {
|
||||||
|
boolean enabled = themeManager.isSoundEnabled();
|
||||||
|
soundToggle.setText(enabled ? "ON" : "OFF");
|
||||||
|
|
||||||
|
GradientDrawable bg = new GradientDrawable();
|
||||||
|
bg.setCornerRadius(dpToPx(8));
|
||||||
|
if (enabled) {
|
||||||
|
bg.setColor(Color.parseColor("#1A22C55E"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#22C55E"));
|
||||||
|
soundToggle.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
} else {
|
||||||
|
bg.setColor(Color.parseColor("#1AEF4444"));
|
||||||
|
bg.setStroke(dpToPx(1), Color.parseColor("#EF4444"));
|
||||||
|
soundToggle.setTextColor(Color.parseColor("#EF4444"));
|
||||||
|
}
|
||||||
|
soundToggle.setBackground(bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Konami Easter Egg
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupKonamiEasterEgg() {
|
||||||
|
if (themeManager.isKonamiUnlocked()) {
|
||||||
|
aboutKonamiHint.setText("✓ Secret wallpapers unlocked");
|
||||||
|
aboutKonamiHint.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
}
|
||||||
|
|
||||||
|
aboutCard.setOnClickListener(v -> {
|
||||||
|
konamiTapCount++;
|
||||||
|
if (konamiTapCount >= 7 && !themeManager.isKonamiUnlocked()) {
|
||||||
|
themeManager.setKonamiUnlocked(true);
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.NOTIFICATION);
|
||||||
|
aboutKonamiHint.setText("✓ Secret wallpapers unlocked");
|
||||||
|
aboutKonamiHint.setTextColor(Color.parseColor("#22C55E"));
|
||||||
|
refreshWallpaperPicker();
|
||||||
|
AeThexToast.show(this, "🔓 Secret wallpapers unlocked!", AeThexToast.Type.SUCCESS);
|
||||||
|
konamiTapCount = 0;
|
||||||
|
} else if (konamiTapCount >= 4 && !themeManager.isKonamiUnlocked()) {
|
||||||
|
int remaining = 7 - konamiTapCount;
|
||||||
|
aboutKonamiHint.setText(remaining + " more taps...");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Device Settings
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupDeviceSettings() {
|
||||||
|
LinearLayout btn = findViewById(R.id.device_settings_btn);
|
||||||
|
btn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
v.animate().scaleX(0.97f).scaleY(0.97f).setDuration(80).withEndAction(() -> {
|
||||||
|
v.animate().scaleX(1f).scaleY(1f).setDuration(80).start();
|
||||||
|
startActivity(new Intent(Settings.ACTION_SETTINGS));
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Hidden Apps
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void setupHiddenApps() {
|
||||||
|
LinearLayout btn = findViewById(R.id.hidden_apps_btn);
|
||||||
|
TextView countLabel = findViewById(R.id.hidden_apps_count);
|
||||||
|
ScreenTimeTracker tracker = new ScreenTimeTracker(this);
|
||||||
|
Set<String> hidden = tracker.getHiddenApps();
|
||||||
|
int count = hidden.size();
|
||||||
|
countLabel.setText(count > 0 ? count + " hidden app" + (count != 1 ? "s" : "") : "No hidden apps");
|
||||||
|
|
||||||
|
btn.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
Set<String> currentHidden = tracker.getHiddenApps();
|
||||||
|
if (currentHidden.isEmpty()) {
|
||||||
|
AeThexToast.show(this, "No hidden apps to restore", AeThexToast.Type.INFO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Unhide all
|
||||||
|
for (String appId : currentHidden) {
|
||||||
|
tracker.setHidden(appId, false);
|
||||||
|
}
|
||||||
|
countLabel.setText("No hidden apps");
|
||||||
|
AeThexToast.show(this, currentHidden.size() + " app(s) restored", AeThexToast.Type.SUCCESS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Utility
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private int dpToPx(int dp) {
|
||||||
|
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
511
android/app/src/main/java/com/aethex/os/SnakeActivity.java
Normal file
511
android/app/src/main/java/com/aethex/os/SnakeActivity.java
Normal file
|
|
@ -0,0 +1,511 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class SnakeActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private SnakeView snakeView;
|
||||||
|
private TextView scoreText;
|
||||||
|
private TextView highScoreText;
|
||||||
|
private TextView statusText;
|
||||||
|
private FrameLayout gameOverOverlay;
|
||||||
|
private TextView finalScoreText;
|
||||||
|
private TextView finalHighScoreText;
|
||||||
|
|
||||||
|
private int score = 0;
|
||||||
|
private int highScore = 0;
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "snake_prefs";
|
||||||
|
private static final String KEY_HIGH_SCORE = "high_score";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_snake);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Load high score
|
||||||
|
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
highScore = prefs.getInt(KEY_HIGH_SCORE, 0);
|
||||||
|
|
||||||
|
// Bind views
|
||||||
|
scoreText = findViewById(R.id.snake_score);
|
||||||
|
highScoreText = findViewById(R.id.snake_high_score);
|
||||||
|
statusText = findViewById(R.id.snake_status);
|
||||||
|
gameOverOverlay = findViewById(R.id.snake_game_over_overlay);
|
||||||
|
finalScoreText = findViewById(R.id.snake_final_score);
|
||||||
|
finalHighScoreText = findViewById(R.id.snake_final_high_score);
|
||||||
|
|
||||||
|
highScoreText.setText("HIGH: " + highScore);
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
findViewById(R.id.snake_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create and add SnakeView
|
||||||
|
FrameLayout container = findViewById(R.id.snake_board_container);
|
||||||
|
snakeView = new SnakeView(this);
|
||||||
|
container.addView(snakeView, new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
// Game over tap to restart
|
||||||
|
gameOverOverlay.setOnClickListener(v -> {
|
||||||
|
gameOverOverlay.setVisibility(View.GONE);
|
||||||
|
snakeView.resetGame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateScore(int newScore) {
|
||||||
|
score = newScore;
|
||||||
|
scoreText.setText("SCORE: " + score);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showGameOver() {
|
||||||
|
if (score > highScore) {
|
||||||
|
highScore = score;
|
||||||
|
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
prefs.edit().putInt(KEY_HIGH_SCORE, highScore).apply();
|
||||||
|
highScoreText.setText("HIGH: " + highScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
finalScoreText.setText("Score: " + score);
|
||||||
|
finalHighScoreText.setText("High Score: " + highScore);
|
||||||
|
statusText.setText("Game over");
|
||||||
|
gameOverOverlay.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if (snakeView != null) {
|
||||||
|
snakeView.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (snakeView != null && snakeView.isRunning) {
|
||||||
|
snakeView.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Inner class: SnakeView - Custom View that renders the snake game
|
||||||
|
// =========================================================================
|
||||||
|
public class SnakeView extends View {
|
||||||
|
|
||||||
|
private static final int GRID_SIZE = 20;
|
||||||
|
private static final int INITIAL_DELAY = 200; // ms between moves
|
||||||
|
private static final int MIN_DELAY = 80; // fastest speed
|
||||||
|
private static final int SPEED_INCREMENT = 3; // ms faster per food eaten
|
||||||
|
|
||||||
|
// Direction constants
|
||||||
|
private static final int DIR_UP = 0;
|
||||||
|
private static final int DIR_RIGHT = 1;
|
||||||
|
private static final int DIR_DOWN = 2;
|
||||||
|
private static final int DIR_LEFT = 3;
|
||||||
|
|
||||||
|
// Game state
|
||||||
|
private LinkedList<int[]> snake; // head is first element
|
||||||
|
private int[] food;
|
||||||
|
private int direction;
|
||||||
|
private int nextDirection;
|
||||||
|
private boolean gameStarted = false;
|
||||||
|
boolean isRunning = false;
|
||||||
|
private boolean isPaused = false;
|
||||||
|
private int currentDelay;
|
||||||
|
|
||||||
|
// Drawing
|
||||||
|
private float cellSize;
|
||||||
|
private float boardOffsetX;
|
||||||
|
private float boardOffsetY;
|
||||||
|
private float boardSize;
|
||||||
|
|
||||||
|
private Paint snakePaint;
|
||||||
|
private Paint snakeHeadPaint;
|
||||||
|
private Paint foodPaint;
|
||||||
|
private Paint foodGlowPaint;
|
||||||
|
private Paint gridPaint;
|
||||||
|
private Paint borderPaint;
|
||||||
|
private Paint bgPaint;
|
||||||
|
private RectF cellRect;
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
private final Runnable gameLoop = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isRunning && !isPaused) {
|
||||||
|
update();
|
||||||
|
invalidate();
|
||||||
|
handler.postDelayed(this, currentDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gesture detection
|
||||||
|
private GestureDetector gestureDetector;
|
||||||
|
private Random random = new Random();
|
||||||
|
|
||||||
|
public SnakeView(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
// Snake body paint - emerald green
|
||||||
|
snakePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
snakePaint.setColor(Color.parseColor("#34D399"));
|
||||||
|
snakePaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
// Snake head paint - slightly brighter
|
||||||
|
snakeHeadPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
snakeHeadPaint.setColor(Color.parseColor("#6EE7B7"));
|
||||||
|
snakeHeadPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
// Food paint - red
|
||||||
|
foodPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
foodPaint.setColor(Color.parseColor("#F87171"));
|
||||||
|
foodPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
// Food glow effect
|
||||||
|
foodGlowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
foodGlowPaint.setColor(Color.parseColor("#33F87171"));
|
||||||
|
foodGlowPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
// Grid lines - very subtle
|
||||||
|
gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
gridPaint.setColor(Color.parseColor("#0D34D399"));
|
||||||
|
gridPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
gridPaint.setStrokeWidth(1f);
|
||||||
|
|
||||||
|
// Board border
|
||||||
|
borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
borderPaint.setColor(Color.parseColor("#1A34D399"));
|
||||||
|
borderPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
borderPaint.setStrokeWidth(2f);
|
||||||
|
|
||||||
|
// Board background
|
||||||
|
bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
bgPaint.setColor(Color.parseColor("#050A0D"));
|
||||||
|
bgPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
cellRect = new RectF();
|
||||||
|
|
||||||
|
// Setup gesture detector for swipe
|
||||||
|
gestureDetector = new GestureDetector(getContext(),
|
||||||
|
new GestureDetector.SimpleOnGestureListener() {
|
||||||
|
|
||||||
|
private static final int SWIPE_THRESHOLD = 30;
|
||||||
|
private static final int SWIPE_VELOCITY_THRESHOLD = 30;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDown(MotionEvent e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFling(MotionEvent e1, MotionEvent e2,
|
||||||
|
float velocityX, float velocityY) {
|
||||||
|
if (e1 == null || e2 == null) return false;
|
||||||
|
|
||||||
|
float diffX = e2.getX() - e1.getX();
|
||||||
|
float diffY = e2.getY() - e1.getY();
|
||||||
|
|
||||||
|
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||||
|
// Horizontal swipe
|
||||||
|
if (Math.abs(diffX) > SWIPE_THRESHOLD &&
|
||||||
|
Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
|
||||||
|
if (diffX > 0) {
|
||||||
|
setDirection(DIR_RIGHT);
|
||||||
|
} else {
|
||||||
|
setDirection(DIR_LEFT);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Vertical swipe
|
||||||
|
if (Math.abs(diffY) > SWIPE_THRESHOLD &&
|
||||||
|
Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
|
||||||
|
if (diffY > 0) {
|
||||||
|
setDirection(DIR_DOWN);
|
||||||
|
} else {
|
||||||
|
setDirection(DIR_UP);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resetGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDirection(int newDir) {
|
||||||
|
// Start game on first swipe
|
||||||
|
if (!gameStarted) {
|
||||||
|
gameStarted = true;
|
||||||
|
isRunning = true;
|
||||||
|
((SnakeActivity) getContext()).statusText.setText("Playing");
|
||||||
|
handler.post(gameLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent 180-degree turns
|
||||||
|
if ((newDir == DIR_UP && direction != DIR_DOWN) ||
|
||||||
|
(newDir == DIR_DOWN && direction != DIR_UP) ||
|
||||||
|
(newDir == DIR_LEFT && direction != DIR_RIGHT) ||
|
||||||
|
(newDir == DIR_RIGHT && direction != DIR_LEFT)) {
|
||||||
|
nextDirection = newDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetGame() {
|
||||||
|
handler.removeCallbacks(gameLoop);
|
||||||
|
|
||||||
|
snake = new LinkedList<>();
|
||||||
|
// Start in center, 3 segments, moving right
|
||||||
|
int centerX = GRID_SIZE / 2;
|
||||||
|
int centerY = GRID_SIZE / 2;
|
||||||
|
snake.add(new int[]{centerX, centerY});
|
||||||
|
snake.add(new int[]{centerX - 1, centerY});
|
||||||
|
snake.add(new int[]{centerX - 2, centerY});
|
||||||
|
|
||||||
|
direction = DIR_RIGHT;
|
||||||
|
nextDirection = DIR_RIGHT;
|
||||||
|
currentDelay = INITIAL_DELAY;
|
||||||
|
gameStarted = false;
|
||||||
|
isRunning = false;
|
||||||
|
isPaused = false;
|
||||||
|
|
||||||
|
spawnFood();
|
||||||
|
|
||||||
|
((SnakeActivity) getContext()).updateScore(0);
|
||||||
|
((SnakeActivity) getContext()).statusText.setText("Swipe to start");
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnFood() {
|
||||||
|
// Build list of all empty cells
|
||||||
|
ArrayList<int[]> emptyCells = new ArrayList<>();
|
||||||
|
for (int x = 0; x < GRID_SIZE; x++) {
|
||||||
|
for (int y = 0; y < GRID_SIZE; y++) {
|
||||||
|
boolean occupied = false;
|
||||||
|
for (int[] segment : snake) {
|
||||||
|
if (segment[0] == x && segment[1] == y) {
|
||||||
|
occupied = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!occupied) {
|
||||||
|
emptyCells.add(new int[]{x, y});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emptyCells.isEmpty()) {
|
||||||
|
food = emptyCells.get(random.nextInt(emptyCells.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update() {
|
||||||
|
direction = nextDirection;
|
||||||
|
|
||||||
|
// Calculate new head position
|
||||||
|
int[] head = snake.getFirst();
|
||||||
|
int newX = head[0];
|
||||||
|
int newY = head[1];
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case DIR_UP: newY--; break;
|
||||||
|
case DIR_DOWN: newY++; break;
|
||||||
|
case DIR_LEFT: newX--; break;
|
||||||
|
case DIR_RIGHT: newX++; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check wall collision
|
||||||
|
if (newX < 0 || newX >= GRID_SIZE || newY < 0 || newY >= GRID_SIZE) {
|
||||||
|
gameOver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check self collision
|
||||||
|
for (int[] segment : snake) {
|
||||||
|
if (segment[0] == newX && segment[1] == newY) {
|
||||||
|
gameOver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new head
|
||||||
|
snake.addFirst(new int[]{newX, newY});
|
||||||
|
|
||||||
|
// Check food collision
|
||||||
|
if (food != null && newX == food[0] && newY == food[1]) {
|
||||||
|
// Ate food - don't remove tail (snake grows)
|
||||||
|
int newScore = ((SnakeActivity) getContext()).score + 10;
|
||||||
|
((SnakeActivity) getContext()).updateScore(newScore);
|
||||||
|
|
||||||
|
// Speed up
|
||||||
|
currentDelay = Math.max(MIN_DELAY, currentDelay - SPEED_INCREMENT);
|
||||||
|
|
||||||
|
// Spawn new food
|
||||||
|
spawnFood();
|
||||||
|
} else {
|
||||||
|
// Remove tail (snake moves)
|
||||||
|
snake.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void gameOver() {
|
||||||
|
isRunning = false;
|
||||||
|
gameStarted = false;
|
||||||
|
handler.removeCallbacks(gameLoop);
|
||||||
|
((SnakeActivity) getContext()).showGameOver();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pause() {
|
||||||
|
isPaused = true;
|
||||||
|
handler.removeCallbacks(gameLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resume() {
|
||||||
|
if (isRunning) {
|
||||||
|
isPaused = false;
|
||||||
|
handler.post(gameLoop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
// Calculate board dimensions to be square and centered
|
||||||
|
boardSize = Math.min(w, h);
|
||||||
|
cellSize = boardSize / GRID_SIZE;
|
||||||
|
boardOffsetX = (w - boardSize) / 2f;
|
||||||
|
boardOffsetY = (h - boardSize) / 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
// Draw board background
|
||||||
|
canvas.drawRect(boardOffsetX, boardOffsetY,
|
||||||
|
boardOffsetX + boardSize, boardOffsetY + boardSize, bgPaint);
|
||||||
|
|
||||||
|
// Draw grid lines
|
||||||
|
for (int i = 0; i <= GRID_SIZE; i++) {
|
||||||
|
float x = boardOffsetX + i * cellSize;
|
||||||
|
float y = boardOffsetY + i * cellSize;
|
||||||
|
// Vertical line
|
||||||
|
canvas.drawLine(x, boardOffsetY, x, boardOffsetY + boardSize, gridPaint);
|
||||||
|
// Horizontal line
|
||||||
|
canvas.drawLine(boardOffsetX, y, boardOffsetX + boardSize, y, gridPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw board border
|
||||||
|
canvas.drawRect(boardOffsetX, boardOffsetY,
|
||||||
|
boardOffsetX + boardSize, boardOffsetY + boardSize, borderPaint);
|
||||||
|
|
||||||
|
float padding = cellSize * 0.08f;
|
||||||
|
float cornerRadius = cellSize * 0.2f;
|
||||||
|
|
||||||
|
// Draw food with glow effect
|
||||||
|
if (food != null) {
|
||||||
|
float foodCX = boardOffsetX + food[0] * cellSize + cellSize / 2f;
|
||||||
|
float foodCY = boardOffsetY + food[1] * cellSize + cellSize / 2f;
|
||||||
|
|
||||||
|
// Outer glow
|
||||||
|
canvas.drawCircle(foodCX, foodCY, cellSize * 0.7f, foodGlowPaint);
|
||||||
|
|
||||||
|
// Food cell
|
||||||
|
cellRect.set(
|
||||||
|
boardOffsetX + food[0] * cellSize + padding,
|
||||||
|
boardOffsetY + food[1] * cellSize + padding,
|
||||||
|
boardOffsetX + (food[0] + 1) * cellSize - padding,
|
||||||
|
boardOffsetY + (food[1] + 1) * cellSize - padding);
|
||||||
|
canvas.drawRoundRect(cellRect, cornerRadius, cornerRadius, foodPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw snake
|
||||||
|
for (int i = 0; i < snake.size(); i++) {
|
||||||
|
int[] segment = snake.get(i);
|
||||||
|
cellRect.set(
|
||||||
|
boardOffsetX + segment[0] * cellSize + padding,
|
||||||
|
boardOffsetY + segment[1] * cellSize + padding,
|
||||||
|
boardOffsetX + (segment[0] + 1) * cellSize - padding,
|
||||||
|
boardOffsetY + (segment[1] + 1) * cellSize - padding);
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
// Head - brighter color
|
||||||
|
canvas.drawRoundRect(cellRect, cornerRadius, cornerRadius, snakeHeadPaint);
|
||||||
|
} else {
|
||||||
|
// Body - with subtle alpha gradient toward tail
|
||||||
|
int alpha = Math.max(120, 255 - (i * 4));
|
||||||
|
snakePaint.setAlpha(alpha);
|
||||||
|
canvas.drawRoundRect(cellRect, cornerRadius, cornerRadius, snakePaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reset alpha
|
||||||
|
snakePaint.setAlpha(255);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
324
android/app/src/main/java/com/aethex/os/SnakeGame.java
Normal file
324
android/app/src/main/java/com/aethex/os/SnakeGame.java
Normal file
|
|
@ -0,0 +1,324 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SnakeGame - Classic snake game for Pixel Arcade module.
|
||||||
|
* Swipe to control direction. Eat food to grow. Don't hit walls or yourself!
|
||||||
|
*/
|
||||||
|
public class SnakeGame extends View {
|
||||||
|
|
||||||
|
private static final int GRID_SIZE = 20;
|
||||||
|
private static final int GAME_SPEED = 150; // ms per frame
|
||||||
|
|
||||||
|
private int gridWidth, gridHeight;
|
||||||
|
private float cellSize;
|
||||||
|
|
||||||
|
private List<int[]> snake = new ArrayList<>();
|
||||||
|
private int[] food = new int[2];
|
||||||
|
private int direction = 0; // 0=right, 1=down, 2=left, 3=up
|
||||||
|
private int nextDirection = 0;
|
||||||
|
private boolean gameOver = false;
|
||||||
|
private boolean gameStarted = false;
|
||||||
|
private int score = 0;
|
||||||
|
private int highScore = 0;
|
||||||
|
|
||||||
|
private Paint snakePaint;
|
||||||
|
private Paint headPaint;
|
||||||
|
private Paint foodPaint;
|
||||||
|
private Paint gridPaint;
|
||||||
|
private Paint textPaint;
|
||||||
|
private Paint overlayPaint;
|
||||||
|
|
||||||
|
private Handler handler;
|
||||||
|
private Runnable gameLoop;
|
||||||
|
private Random random = new Random();
|
||||||
|
|
||||||
|
private float touchStartX, touchStartY;
|
||||||
|
private int primaryColor;
|
||||||
|
|
||||||
|
public SnakeGame(Context context) {
|
||||||
|
super(context);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SnakeGame(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context) {
|
||||||
|
ThemeManager tm = new ThemeManager(context);
|
||||||
|
primaryColor = tm.getPrimaryColor(context);
|
||||||
|
|
||||||
|
snakePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
snakePaint.setColor(primaryColor);
|
||||||
|
|
||||||
|
headPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
headPaint.setColor(Color.WHITE);
|
||||||
|
|
||||||
|
foodPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
foodPaint.setColor(Color.parseColor("#EF4444"));
|
||||||
|
|
||||||
|
gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
gridPaint.setColor(Color.parseColor("#1AFFFFFF"));
|
||||||
|
gridPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
gridPaint.setStrokeWidth(1);
|
||||||
|
|
||||||
|
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
textPaint.setColor(Color.WHITE);
|
||||||
|
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
|
||||||
|
overlayPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
overlayPaint.setColor(Color.parseColor("#CC000000"));
|
||||||
|
|
||||||
|
handler = new Handler(Looper.getMainLooper());
|
||||||
|
gameLoop = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (!gameOver && gameStarted) {
|
||||||
|
update();
|
||||||
|
invalidate();
|
||||||
|
handler.postDelayed(this, GAME_SPEED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load high score
|
||||||
|
highScore = context.getSharedPreferences("aethex_arcade", Context.MODE_PRIVATE)
|
||||||
|
.getInt("snake_highscore", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
cellSize = Math.min(w, h) / (float) GRID_SIZE;
|
||||||
|
gridWidth = (int) (w / cellSize);
|
||||||
|
gridHeight = (int) (h / cellSize);
|
||||||
|
resetGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetGame() {
|
||||||
|
snake.clear();
|
||||||
|
// Start in center
|
||||||
|
int startX = gridWidth / 2;
|
||||||
|
int startY = gridHeight / 2;
|
||||||
|
snake.add(new int[]{startX, startY});
|
||||||
|
snake.add(new int[]{startX - 1, startY});
|
||||||
|
snake.add(new int[]{startX - 2, startY});
|
||||||
|
|
||||||
|
direction = 0;
|
||||||
|
nextDirection = 0;
|
||||||
|
gameOver = false;
|
||||||
|
gameStarted = false;
|
||||||
|
score = 0;
|
||||||
|
spawnFood();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnFood() {
|
||||||
|
boolean valid;
|
||||||
|
do {
|
||||||
|
food[0] = random.nextInt(gridWidth);
|
||||||
|
food[1] = random.nextInt(gridHeight);
|
||||||
|
valid = true;
|
||||||
|
for (int[] seg : snake) {
|
||||||
|
if (seg[0] == food[0] && seg[1] == food[1]) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (!valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update() {
|
||||||
|
direction = nextDirection;
|
||||||
|
|
||||||
|
int[] head = snake.get(0);
|
||||||
|
int newX = head[0];
|
||||||
|
int newY = head[1];
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case 0: newX++; break; // right
|
||||||
|
case 1: newY++; break; // down
|
||||||
|
case 2: newX--; break; // left
|
||||||
|
case 3: newY--; break; // up
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wall collision
|
||||||
|
if (newX < 0 || newX >= gridWidth || newY < 0 || newY >= gridHeight) {
|
||||||
|
endGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self collision
|
||||||
|
for (int[] seg : snake) {
|
||||||
|
if (seg[0] == newX && seg[1] == newY) {
|
||||||
|
endGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move snake
|
||||||
|
snake.add(0, new int[]{newX, newY});
|
||||||
|
|
||||||
|
// Check food
|
||||||
|
if (newX == food[0] && newY == food[1]) {
|
||||||
|
score += 10;
|
||||||
|
spawnFood();
|
||||||
|
} else {
|
||||||
|
snake.remove(snake.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endGame() {
|
||||||
|
gameOver = true;
|
||||||
|
if (score > highScore) {
|
||||||
|
highScore = score;
|
||||||
|
getContext().getSharedPreferences("aethex_arcade", Context.MODE_PRIVATE)
|
||||||
|
.edit().putInt("snake_highscore", highScore).apply();
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
// Background
|
||||||
|
canvas.drawColor(Color.parseColor("#0A0C14"));
|
||||||
|
|
||||||
|
// Grid
|
||||||
|
for (int x = 0; x <= gridWidth; x++) {
|
||||||
|
canvas.drawLine(x * cellSize, 0, x * cellSize, gridHeight * cellSize, gridPaint);
|
||||||
|
}
|
||||||
|
for (int y = 0; y <= gridHeight; y++) {
|
||||||
|
canvas.drawLine(0, y * cellSize, gridWidth * cellSize, y * cellSize, gridPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Food with glow
|
||||||
|
float fx = food[0] * cellSize + cellSize / 2;
|
||||||
|
float fy = food[1] * cellSize + cellSize / 2;
|
||||||
|
foodPaint.setAlpha(60);
|
||||||
|
canvas.drawCircle(fx, fy, cellSize * 0.6f, foodPaint);
|
||||||
|
foodPaint.setAlpha(255);
|
||||||
|
canvas.drawCircle(fx, fy, cellSize * 0.35f, foodPaint);
|
||||||
|
|
||||||
|
// Snake
|
||||||
|
float cornerRadius = cellSize * 0.3f;
|
||||||
|
for (int i = 0; i < snake.size(); i++) {
|
||||||
|
int[] seg = snake.get(i);
|
||||||
|
RectF rect = new RectF(
|
||||||
|
seg[0] * cellSize + 2,
|
||||||
|
seg[1] * cellSize + 2,
|
||||||
|
(seg[0] + 1) * cellSize - 2,
|
||||||
|
(seg[1] + 1) * cellSize - 2
|
||||||
|
);
|
||||||
|
if (i == 0) {
|
||||||
|
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, headPaint);
|
||||||
|
} else {
|
||||||
|
int alpha = 255 - (i * 8);
|
||||||
|
snakePaint.setAlpha(Math.max(100, alpha));
|
||||||
|
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, snakePaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snakePaint.setAlpha(255);
|
||||||
|
|
||||||
|
// Score
|
||||||
|
textPaint.setTextSize(cellSize * 0.8f);
|
||||||
|
textPaint.setColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
canvas.drawText("SCORE: " + score, getWidth() / 2f, cellSize * 1.2f, textPaint);
|
||||||
|
|
||||||
|
// High score
|
||||||
|
textPaint.setTextSize(cellSize * 0.5f);
|
||||||
|
canvas.drawText("HI: " + highScore, getWidth() / 2f, cellSize * 1.9f, textPaint);
|
||||||
|
|
||||||
|
// Game over / Start overlay
|
||||||
|
if (gameOver || !gameStarted) {
|
||||||
|
canvas.drawRect(0, 0, getWidth(), getHeight(), overlayPaint);
|
||||||
|
textPaint.setColor(Color.WHITE);
|
||||||
|
|
||||||
|
if (gameOver) {
|
||||||
|
textPaint.setTextSize(cellSize * 1.5f);
|
||||||
|
canvas.drawText("GAME OVER", getWidth() / 2f, getHeight() / 2f - cellSize, textPaint);
|
||||||
|
textPaint.setTextSize(cellSize);
|
||||||
|
canvas.drawText("Score: " + score, getWidth() / 2f, getHeight() / 2f + cellSize * 0.5f, textPaint);
|
||||||
|
if (score >= highScore && score > 0) {
|
||||||
|
textPaint.setColor(Color.parseColor("#FBBF24"));
|
||||||
|
canvas.drawText("NEW HIGH SCORE!", getWidth() / 2f, getHeight() / 2f + cellSize * 1.8f, textPaint);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textPaint.setTextSize(cellSize * 1.5f);
|
||||||
|
textPaint.setColor(primaryColor);
|
||||||
|
canvas.drawText("SNAKE", getWidth() / 2f, getHeight() / 2f - cellSize * 2, textPaint);
|
||||||
|
textPaint.setTextSize(cellSize * 0.7f);
|
||||||
|
textPaint.setColor(Color.parseColor("#88FFFFFF"));
|
||||||
|
canvas.drawText("Swipe to control", getWidth() / 2f, getHeight() / 2f, textPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
textPaint.setTextSize(cellSize * 0.6f);
|
||||||
|
textPaint.setColor(Color.parseColor("#66FFFFFF"));
|
||||||
|
canvas.drawText("TAP TO " + (gameOver ? "RESTART" : "START"), getWidth() / 2f, getHeight() - cellSize * 2, textPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
touchStartX = event.getX();
|
||||||
|
touchStartY = event.getY();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
float dx = event.getX() - touchStartX;
|
||||||
|
float dy = event.getY() - touchStartY;
|
||||||
|
|
||||||
|
if (Math.abs(dx) < 30 && Math.abs(dy) < 30) {
|
||||||
|
// Tap
|
||||||
|
if (gameOver) {
|
||||||
|
resetGame();
|
||||||
|
} else if (!gameStarted) {
|
||||||
|
gameStarted = true;
|
||||||
|
handler.post(gameLoop);
|
||||||
|
}
|
||||||
|
} else if (gameStarted && !gameOver) {
|
||||||
|
// Swipe
|
||||||
|
if (Math.abs(dx) > Math.abs(dy)) {
|
||||||
|
// Horizontal swipe
|
||||||
|
if (dx > 0 && direction != 2) nextDirection = 0;
|
||||||
|
else if (dx < 0 && direction != 0) nextDirection = 2;
|
||||||
|
} else {
|
||||||
|
// Vertical swipe
|
||||||
|
if (dy > 0 && direction != 3) nextDirection = 1;
|
||||||
|
else if (dy < 0 && direction != 1) nextDirection = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
handler.removeCallbacks(gameLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resume() {
|
||||||
|
if (gameStarted && !gameOver) {
|
||||||
|
handler.post(gameLoop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
android/app/src/main/java/com/aethex/os/SoundManager.java
Normal file
162
android/app/src/main/java/com/aethex/os/SoundManager.java
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.media.AudioAttributes;
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioTrack;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton sound manager for AeThexOS Android.
|
||||||
|
* Generates tones programmatically using AudioTrack - no sound asset files needed.
|
||||||
|
*/
|
||||||
|
public class SoundManager {
|
||||||
|
|
||||||
|
private static final int SAMPLE_RATE = 44100;
|
||||||
|
private static final float VOLUME = 0.3f;
|
||||||
|
|
||||||
|
public enum Sound {
|
||||||
|
OPEN(523, 0.1, WaveType.SINE),
|
||||||
|
CLOSE(392, 0.1, WaveType.SINE),
|
||||||
|
CLICK(800, 0.03, WaveType.SQUARE),
|
||||||
|
NOTIFICATION(880, 0.15, WaveType.SINE),
|
||||||
|
SWITCH(440, 0.2, WaveType.SAWTOOTH),
|
||||||
|
BOOT_BEEP(660, 0.05, WaveType.SINE);
|
||||||
|
|
||||||
|
final int frequency;
|
||||||
|
final double duration;
|
||||||
|
final WaveType waveType;
|
||||||
|
|
||||||
|
Sound(int frequency, double duration, WaveType waveType) {
|
||||||
|
this.frequency = frequency;
|
||||||
|
this.duration = duration;
|
||||||
|
this.waveType = waveType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum WaveType {
|
||||||
|
SINE, SQUARE, SAWTOOTH
|
||||||
|
}
|
||||||
|
|
||||||
|
private static volatile SoundManager instance;
|
||||||
|
|
||||||
|
private final Map<Sound, byte[]> bufferCache = new EnumMap<>(Sound.class);
|
||||||
|
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
private volatile boolean enabled = true;
|
||||||
|
|
||||||
|
private SoundManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SoundManager getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (SoundManager.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new SoundManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a sound on a background thread. Non-blocking.
|
||||||
|
*/
|
||||||
|
public void play(Sound sound) {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
byte[] pcm = getOrGenerateBuffer(sound);
|
||||||
|
int bufferSize = pcm.length;
|
||||||
|
|
||||||
|
AudioAttributes attributes = new AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AudioFormat format = new AudioFormat.Builder()
|
||||||
|
.setSampleRate(SAMPLE_RATE)
|
||||||
|
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||||
|
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AudioTrack track = new AudioTrack.Builder()
|
||||||
|
.setAudioAttributes(attributes)
|
||||||
|
.setAudioFormat(format)
|
||||||
|
.setBufferSizeInBytes(bufferSize)
|
||||||
|
.setTransferMode(AudioTrack.MODE_STATIC)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
track.write(pcm, 0, pcm.length);
|
||||||
|
track.setVolume(VOLUME);
|
||||||
|
track.play();
|
||||||
|
|
||||||
|
// Wait for playback to complete, then release
|
||||||
|
long durationMs = (long) (sound.duration * 1000) + 50;
|
||||||
|
Thread.sleep(durationMs);
|
||||||
|
|
||||||
|
track.stop();
|
||||||
|
track.release();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Silently ignore sound playback failures
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized byte[] getOrGenerateBuffer(Sound sound) {
|
||||||
|
byte[] cached = bufferCache.get(sound);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
|
||||||
|
byte[] buffer = generatePcmBuffer(sound.frequency, sound.duration, sound.waveType);
|
||||||
|
bufferCache.put(sound, buffer);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generatePcmBuffer(int frequency, double duration, WaveType waveType) {
|
||||||
|
int numSamples = (int) (SAMPLE_RATE * duration);
|
||||||
|
byte[] pcm = new byte[numSamples * 2]; // 16-bit mono = 2 bytes per sample
|
||||||
|
|
||||||
|
for (int i = 0; i < numSamples; i++) {
|
||||||
|
double t = (double) i / SAMPLE_RATE;
|
||||||
|
double sample;
|
||||||
|
|
||||||
|
switch (waveType) {
|
||||||
|
case SQUARE:
|
||||||
|
sample = Math.signum(Math.sin(2.0 * Math.PI * frequency * t));
|
||||||
|
break;
|
||||||
|
case SAWTOOTH:
|
||||||
|
sample = 2.0 * (t * frequency - Math.floor(0.5 + t * frequency));
|
||||||
|
break;
|
||||||
|
case SINE:
|
||||||
|
default:
|
||||||
|
sample = Math.sin(2.0 * Math.PI * frequency * t);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a short fade-in/fade-out envelope to avoid clicks
|
||||||
|
int fadeLength = Math.min(numSamples / 10, SAMPLE_RATE / 200);
|
||||||
|
if (i < fadeLength) {
|
||||||
|
sample *= (double) i / fadeLength;
|
||||||
|
} else if (i > numSamples - fadeLength) {
|
||||||
|
sample *= (double) (numSamples - i) / fadeLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
short pcmValue = (short) (sample * Short.MAX_VALUE);
|
||||||
|
pcm[i * 2] = (byte) (pcmValue & 0xFF);
|
||||||
|
pcm[i * 2 + 1] = (byte) ((pcmValue >> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pcm;
|
||||||
|
}
|
||||||
|
}
|
||||||
2281
android/app/src/main/java/com/aethex/os/SystemActivity.java
Normal file
2281
android/app/src/main/java/com/aethex/os/SystemActivity.java
Normal file
File diff suppressed because it is too large
Load diff
559
android/app/src/main/java/com/aethex/os/TerminalActivity.java
Normal file
559
android/app/src/main/java/com/aethex/os/TerminalActivity.java
Normal file
|
|
@ -0,0 +1,559 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import java.io.File;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AeThexOS Terminal - Real root shell with full system access.
|
||||||
|
* Execute actual Linux commands on the device.
|
||||||
|
*/
|
||||||
|
public class TerminalActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
private TextView outputView;
|
||||||
|
private EditText inputView;
|
||||||
|
private ScrollView scrollView;
|
||||||
|
private Handler mainHandler;
|
||||||
|
|
||||||
|
// Current working directory (simulated)
|
||||||
|
private String currentDir = "/sdcard";
|
||||||
|
|
||||||
|
// Command history
|
||||||
|
private List<String> commandHistory = new ArrayList<>();
|
||||||
|
private int historyIndex = -1;
|
||||||
|
|
||||||
|
// Root status
|
||||||
|
private boolean hasRoot = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_terminal);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
outputView = findViewById(R.id.terminal_output);
|
||||||
|
inputView = findViewById(R.id.terminal_input);
|
||||||
|
scrollView = findViewById(R.id.terminal_scroll);
|
||||||
|
|
||||||
|
// Apply Source Code Pro font
|
||||||
|
outputView.setTypeface(themeManager.getMonoFont(this));
|
||||||
|
inputView.setTypeface(themeManager.getMonoFont(this));
|
||||||
|
|
||||||
|
findViewById(R.id.terminal_back).setOnClickListener(v -> {
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check root status
|
||||||
|
new Thread(() -> {
|
||||||
|
hasRoot = RootShell.isRootAvailable();
|
||||||
|
mainHandler.post(this::showWelcome);
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
// Handle enter key
|
||||||
|
inputView.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_SEND ||
|
||||||
|
(event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||||
|
&& event.getAction() == KeyEvent.ACTION_DOWN)) {
|
||||||
|
String cmd = inputView.getText().toString().trim();
|
||||||
|
if (!cmd.isEmpty()) {
|
||||||
|
commandHistory.add(cmd);
|
||||||
|
historyIndex = commandHistory.size();
|
||||||
|
processCommand(cmd);
|
||||||
|
inputView.setText("");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
inputView.requestFocus();
|
||||||
|
AeThexKeyboard.attach(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showWelcome() {
|
||||||
|
appendColored("╔══════════════════════════════════════════╗", "#06B6D4");
|
||||||
|
appendColored("║ AeThexOS Terminal v2.1.0 [ROOT] ║", "#06B6D4");
|
||||||
|
appendColored("╚══════════════════════════════════════════╝", "#06B6D4");
|
||||||
|
appendOutput("");
|
||||||
|
if (hasRoot) {
|
||||||
|
appendColored(" ✓ ROOT ACCESS GRANTED", "#22C55E");
|
||||||
|
appendColored(" ✓ Full system privileges enabled", "#22C55E");
|
||||||
|
} else {
|
||||||
|
appendColored(" ⚠ Root access unavailable", "#FBBF24");
|
||||||
|
appendColored(" ⚠ Running in restricted mode", "#FBBF24");
|
||||||
|
}
|
||||||
|
// Check Terminal Pro
|
||||||
|
ModuleManager modules = ModuleManager.getInstance(this);
|
||||||
|
if (modules.hasTerminalPro()) {
|
||||||
|
appendColored(" ★ Terminal Pro features enabled", "#A855F7");
|
||||||
|
}
|
||||||
|
appendOutput("");
|
||||||
|
appendColored("Type 'help' for commands, or run any Linux command.", "#66FFFFFF");
|
||||||
|
appendColored("Prefix with 'su' for root execution.\n", "#66FFFFFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processCommand(String cmd) {
|
||||||
|
String prompt = hasRoot ? "root@aethex:~# " : "user@aethex:~$ ";
|
||||||
|
appendColored(prompt + cmd, "#06B6D4");
|
||||||
|
|
||||||
|
String[] parts = cmd.split("\\s+");
|
||||||
|
String command = parts[0].toLowerCase();
|
||||||
|
|
||||||
|
// Terminal Pro aliases
|
||||||
|
ModuleManager modules = ModuleManager.getInstance(this);
|
||||||
|
if (modules.hasTerminalPro()) {
|
||||||
|
// Expand aliases
|
||||||
|
switch (command) {
|
||||||
|
case "ll": cmd = "ls -la"; parts = cmd.split("\\s+"); command = "ls"; break;
|
||||||
|
case "la": cmd = "ls -a"; parts = cmd.split("\\s+"); command = "ls"; break;
|
||||||
|
case "l": cmd = "ls -l"; parts = cmd.split("\\s+"); command = "ls"; break;
|
||||||
|
case "cls": outputView.setText(""); return;
|
||||||
|
case "..": cmd = "cd .."; parts = cmd.split("\\s+"); command = "cd"; break;
|
||||||
|
case "c": cmd = "clear"; command = "clear"; break;
|
||||||
|
case "h": cmd = "history"; command = "history"; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in commands
|
||||||
|
switch (command) {
|
||||||
|
case "help":
|
||||||
|
showHelp();
|
||||||
|
return;
|
||||||
|
case "clear":
|
||||||
|
case "cls":
|
||||||
|
outputView.setText("");
|
||||||
|
return;
|
||||||
|
case "exit":
|
||||||
|
case "quit":
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
return;
|
||||||
|
case "neofetch":
|
||||||
|
showNeofetch();
|
||||||
|
return;
|
||||||
|
case "rootcheck":
|
||||||
|
checkRoot();
|
||||||
|
return;
|
||||||
|
case "sysinfo":
|
||||||
|
showSysInfo();
|
||||||
|
return;
|
||||||
|
case "processes":
|
||||||
|
case "ps":
|
||||||
|
showProcesses();
|
||||||
|
return;
|
||||||
|
case "netinfo":
|
||||||
|
showNetInfo();
|
||||||
|
return;
|
||||||
|
case "cpuinfo":
|
||||||
|
showCpuInfo();
|
||||||
|
return;
|
||||||
|
case "meminfo":
|
||||||
|
showMemInfo();
|
||||||
|
return;
|
||||||
|
case "diskinfo":
|
||||||
|
case "df":
|
||||||
|
showDiskInfo();
|
||||||
|
return;
|
||||||
|
case "mounts":
|
||||||
|
showMounts();
|
||||||
|
return;
|
||||||
|
case "selinux":
|
||||||
|
showSelinux();
|
||||||
|
return;
|
||||||
|
case "getprop":
|
||||||
|
if (parts.length > 1) {
|
||||||
|
executeAsync(cmd, false);
|
||||||
|
} else {
|
||||||
|
appendColored("Usage: getprop <property>", "#FBBF24");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "setprop":
|
||||||
|
if (parts.length > 2) {
|
||||||
|
executeAsync(cmd, true);
|
||||||
|
} else {
|
||||||
|
appendColored("Usage: setprop <property> <value>", "#FBBF24");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "remount":
|
||||||
|
remountSystem();
|
||||||
|
return;
|
||||||
|
case "reboot":
|
||||||
|
rebootDevice(parts);
|
||||||
|
return;
|
||||||
|
case "dmesg":
|
||||||
|
executeAsync("dmesg | tail -50", true);
|
||||||
|
return;
|
||||||
|
case "logcat":
|
||||||
|
executeAsync("logcat -d -t 30", false);
|
||||||
|
return;
|
||||||
|
case "su":
|
||||||
|
if (parts.length > 1) {
|
||||||
|
// Execute rest of command as root
|
||||||
|
String rootCmd = cmd.substring(3).trim();
|
||||||
|
executeAsync(rootCmd, true);
|
||||||
|
} else {
|
||||||
|
if (hasRoot) {
|
||||||
|
appendColored("Root shell active. All commands run as root.", "#22C55E");
|
||||||
|
} else {
|
||||||
|
appendColored("Root access denied.", "#EF4444");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "shell":
|
||||||
|
case "sh":
|
||||||
|
if (parts.length > 1) {
|
||||||
|
String shellCmd = cmd.substring(command.length()).trim();
|
||||||
|
executeAsync(shellCmd, false);
|
||||||
|
} else {
|
||||||
|
appendColored("Usage: sh <command>", "#FBBF24");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "history":
|
||||||
|
showHistory();
|
||||||
|
return;
|
||||||
|
case "modules":
|
||||||
|
showModules();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute as real shell command
|
||||||
|
executeAsync(cmd, hasRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHistory() {
|
||||||
|
ModuleManager modules = ModuleManager.getInstance(this);
|
||||||
|
if (!modules.hasTerminalPro()) {
|
||||||
|
appendColored("⚠ Terminal Pro required for command history", "#FBBF24");
|
||||||
|
appendColored(" Install from Marketplace to unlock", "#66FFFFFF");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appendColored("\n═══ Command History ═══", "#A855F7");
|
||||||
|
if (commandHistory.isEmpty()) {
|
||||||
|
appendOutput(" (no commands yet)");
|
||||||
|
} else {
|
||||||
|
int start = Math.max(0, commandHistory.size() - 20);
|
||||||
|
for (int i = start; i < commandHistory.size(); i++) {
|
||||||
|
appendOutput(" " + (i + 1) + " " + commandHistory.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendOutput("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showModules() {
|
||||||
|
ModuleManager mm = ModuleManager.getInstance(this);
|
||||||
|
appendColored("\n═══ Installed Modules ═══", "#A855F7");
|
||||||
|
appendOutput(" " + mm.getInstalledCount() + " modules installed");
|
||||||
|
appendOutput(" " + mm.getTotalCreditsSpent() + " Cr spent");
|
||||||
|
appendOutput("");
|
||||||
|
if (mm.hasTerminalPro()) appendColored(" ✓ Terminal Pro", "#22C55E");
|
||||||
|
if (mm.hasDataAnalyzer()) appendColored(" ✓ Data Analyzer", "#22C55E");
|
||||||
|
if (mm.hasNetScanner()) appendColored(" ✓ Net Scanner", "#22C55E");
|
||||||
|
if (mm.hasHexEditor()) appendColored(" ✓ Hex Editor", "#22C55E");
|
||||||
|
if (mm.hasEncryption()) appendColored(" ✓ Cipher Module", "#22C55E");
|
||||||
|
if (mm.hasAppLock()) appendColored(" ✓ Shield Guard", "#22C55E");
|
||||||
|
if (mm.hasStealthMode()) appendColored(" ✓ Ghost Protocol", "#22C55E");
|
||||||
|
if (mm.hasBiometricLock()) appendColored(" ✓ Vault Lock", "#22C55E");
|
||||||
|
if (mm.hasNeonThemes()) appendColored(" ✓ Neon Theme Pack", "#22C55E");
|
||||||
|
if (mm.hasCRTOverlay()) appendColored(" ✓ Retro CRT Skin", "#22C55E");
|
||||||
|
if (mm.hasHoloGlass()) appendColored(" ✓ Holo Glass UI", "#22C55E");
|
||||||
|
if (mm.hasSoundPackPro()) appendColored(" ✓ Sound Pack Pro", "#22C55E");
|
||||||
|
if (mm.hasPixelArcade()) appendColored(" ✓ Pixel Arcade", "#22C55E");
|
||||||
|
if (mm.hasSynthStudio()) appendColored(" ✓ Synth Studio", "#22C55E");
|
||||||
|
appendOutput("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHelp() {
|
||||||
|
ModuleManager modules = ModuleManager.getInstance(this);
|
||||||
|
boolean hasPro = modules.hasTerminalPro();
|
||||||
|
|
||||||
|
appendColored("\n═══ AeThexOS Terminal Commands ═══", "#06B6D4");
|
||||||
|
appendOutput("");
|
||||||
|
appendColored("BUILT-IN:", "#FBBF24");
|
||||||
|
appendOutput(" help Show this help");
|
||||||
|
appendOutput(" clear Clear terminal");
|
||||||
|
appendOutput(" exit Close terminal");
|
||||||
|
appendOutput(" neofetch System overview");
|
||||||
|
appendOutput(" modules Show installed modules");
|
||||||
|
appendOutput("");
|
||||||
|
appendColored("SYSTEM INFO:", "#FBBF24");
|
||||||
|
appendOutput(" sysinfo Full system information");
|
||||||
|
appendOutput(" cpuinfo CPU details");
|
||||||
|
appendOutput(" meminfo Memory statistics");
|
||||||
|
appendOutput(" diskinfo Storage usage");
|
||||||
|
appendOutput(" processes Running processes");
|
||||||
|
appendOutput(" netinfo Network interfaces");
|
||||||
|
appendOutput(" mounts Mounted filesystems");
|
||||||
|
appendOutput(" selinux SELinux status");
|
||||||
|
appendOutput(" rootcheck Check root access");
|
||||||
|
appendOutput("");
|
||||||
|
appendColored("ROOT COMMANDS:", "#EF4444");
|
||||||
|
appendOutput(" su <cmd> Execute as root");
|
||||||
|
appendOutput(" remount Remount /system r/w");
|
||||||
|
appendOutput(" reboot Reboot device");
|
||||||
|
appendOutput(" reboot recovery");
|
||||||
|
appendOutput(" reboot bootloader");
|
||||||
|
appendOutput(" dmesg Kernel messages");
|
||||||
|
appendOutput(" getprop Get system property");
|
||||||
|
appendOutput(" setprop Set system property");
|
||||||
|
appendOutput("");
|
||||||
|
if (hasPro) {
|
||||||
|
appendColored("TERMINAL PRO:", "#A855F7");
|
||||||
|
appendOutput(" history Command history");
|
||||||
|
appendOutput(" ll Alias for ls -la");
|
||||||
|
appendOutput(" la Alias for ls -a");
|
||||||
|
appendOutput(" l Alias for ls -l");
|
||||||
|
appendOutput(" c Alias for clear");
|
||||||
|
appendOutput(" h Alias for history");
|
||||||
|
appendOutput(" .. Alias for cd ..");
|
||||||
|
appendOutput("");
|
||||||
|
}
|
||||||
|
appendColored("SHELL:", "#22C55E");
|
||||||
|
appendOutput(" Any Linux command will be executed");
|
||||||
|
appendOutput(" Examples: ls, cat, grep, find, etc.");
|
||||||
|
appendOutput("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNeofetch() {
|
||||||
|
ThemeManager tm = new ThemeManager(this);
|
||||||
|
String clearance = tm.isFoundation() ? "Foundation" : "Corp";
|
||||||
|
String rootStatus = hasRoot ? "Enabled" : "Disabled";
|
||||||
|
String selinux = RootShell.getSelinuxStatus();
|
||||||
|
|
||||||
|
appendColored("", "#06B6D4");
|
||||||
|
appendColored(" ▄▀▀▀▀▄ " + "root@aethex", "#06B6D4");
|
||||||
|
appendColored(" █ ▄▄▄▄ █ " + "─────────────────────", "#06B6D4");
|
||||||
|
appendColored(" █ █ █ █ " + "OS: AeThex OS v2.1.0", "#FFFFFF");
|
||||||
|
appendColored(" █ ▀▀▀▀ █ " + "Host: " + android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL, "#FFFFFF");
|
||||||
|
appendColored(" ▀▄▄▄▄▀ " + "Kernel: " + RootShell.executeNonRoot("uname -r"), "#FFFFFF");
|
||||||
|
appendColored(" ▄█████▄ " + "Uptime: " + getUptimeString(), "#FFFFFF");
|
||||||
|
appendColored(" ▀███████▀ " + "Shell: AeThex Terminal", "#FFFFFF");
|
||||||
|
appendColored(" █ █ █ " + "Root: " + rootStatus, hasRoot ? "#22C55E" : "#EF4444");
|
||||||
|
appendColored(" ▀▀▀▀▀▀▀ " + "SELinux: " + selinux, "#FFFFFF");
|
||||||
|
appendColored(" " + "Android: " + android.os.Build.VERSION.RELEASE, "#FFFFFF");
|
||||||
|
appendColored(" " + "Clearance: " + clearance, tm.isFoundation() ? "#06B6D4" : "#A855F7");
|
||||||
|
appendOutput("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkRoot() {
|
||||||
|
appendColored("\n═══ Root Status Check ═══", "#06B6D4");
|
||||||
|
|
||||||
|
// Check su binary
|
||||||
|
appendOutput("Checking su binary...");
|
||||||
|
boolean suExists = new File("/system/bin/su").exists() ||
|
||||||
|
new File("/system/xbin/su").exists() ||
|
||||||
|
new File("/sbin/su").exists();
|
||||||
|
|
||||||
|
if (suExists) {
|
||||||
|
appendColored(" ✓ su binary found", "#22C55E");
|
||||||
|
} else {
|
||||||
|
appendColored(" ✗ su binary not found", "#EF4444");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check root access
|
||||||
|
appendOutput("Testing root access...");
|
||||||
|
if (hasRoot) {
|
||||||
|
appendColored(" ✓ Root access confirmed (uid=0)", "#22C55E");
|
||||||
|
String whoami = RootShell.execute("whoami");
|
||||||
|
appendOutput(" Running as: " + whoami);
|
||||||
|
} else {
|
||||||
|
appendColored(" ✗ Root access denied", "#EF4444");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check busybox
|
||||||
|
appendOutput("Checking busybox...");
|
||||||
|
String bbCheck = RootShell.executeNonRoot("which busybox 2>/dev/null");
|
||||||
|
if (!bbCheck.isEmpty() && !bbCheck.startsWith("Error")) {
|
||||||
|
appendColored(" ✓ Busybox found: " + bbCheck, "#22C55E");
|
||||||
|
} else {
|
||||||
|
appendColored(" - Busybox not installed", "#FBBF24");
|
||||||
|
}
|
||||||
|
|
||||||
|
appendOutput("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSysInfo() {
|
||||||
|
appendColored("\n═══ System Information ═══", "#06B6D4");
|
||||||
|
appendOutput("");
|
||||||
|
appendOutput("Device: " + android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL);
|
||||||
|
appendOutput("Product: " + android.os.Build.PRODUCT);
|
||||||
|
appendOutput("Board: " + android.os.Build.BOARD);
|
||||||
|
appendOutput("Hardware: " + android.os.Build.HARDWARE);
|
||||||
|
appendOutput("Android: " + android.os.Build.VERSION.RELEASE + " (SDK " + android.os.Build.VERSION.SDK_INT + ")");
|
||||||
|
appendOutput("Build: " + android.os.Build.DISPLAY);
|
||||||
|
appendOutput("Fingerprint: " + android.os.Build.FINGERPRINT);
|
||||||
|
appendOutput("Kernel: " + RootShell.executeNonRoot("uname -r"));
|
||||||
|
appendOutput("Architecture: " + android.os.Build.SUPPORTED_ABIS[0]);
|
||||||
|
appendOutput("Root: " + (hasRoot ? "Yes" : "No"));
|
||||||
|
appendOutput("SELinux: " + RootShell.getSelinuxStatus());
|
||||||
|
appendOutput("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showProcesses() {
|
||||||
|
appendColored("\n═══ Running Processes ═══", "#06B6D4");
|
||||||
|
executeAsync("ps -A 2>/dev/null | head -25 || ps | head -20", hasRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNetInfo() {
|
||||||
|
appendColored("\n═══ Network Interfaces ═══", "#06B6D4");
|
||||||
|
executeAsync("ip addr 2>/dev/null || ifconfig 2>/dev/null", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCpuInfo() {
|
||||||
|
appendColored("\n═══ CPU Information ═══", "#06B6D4");
|
||||||
|
executeAsync("cat /proc/cpuinfo | head -25", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMemInfo() {
|
||||||
|
appendColored("\n═══ Memory Information ═══", "#06B6D4");
|
||||||
|
executeAsync("cat /proc/meminfo | head -15", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDiskInfo() {
|
||||||
|
appendColored("\n═══ Disk Usage ═══", "#06B6D4");
|
||||||
|
executeAsync("df -h 2>/dev/null || df", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMounts() {
|
||||||
|
appendColored("\n═══ Mounted Filesystems ═══", "#06B6D4");
|
||||||
|
executeAsync("mount | head -20", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSelinux() {
|
||||||
|
appendColored("\n═══ SELinux Status ═══", "#06B6D4");
|
||||||
|
appendOutput("Status: " + RootShell.getSelinuxStatus());
|
||||||
|
if (hasRoot) {
|
||||||
|
executeAsync("cat /sys/fs/selinux/enforce 2>/dev/null && echo '(1=Enforcing, 0=Permissive)'", true);
|
||||||
|
}
|
||||||
|
appendOutput("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void remountSystem() {
|
||||||
|
if (!hasRoot) {
|
||||||
|
appendColored("Error: Root access required", "#EF4444");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appendColored("Remounting /system as read-write...", "#FBBF24");
|
||||||
|
executeAsync("mount -o rw,remount /system 2>&1", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebootDevice(String[] parts) {
|
||||||
|
if (!hasRoot) {
|
||||||
|
appendColored("Error: Root access required", "#EF4444");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String mode = parts.length > 1 ? parts[1].toLowerCase() : "normal";
|
||||||
|
String cmd;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case "recovery":
|
||||||
|
appendColored("Rebooting to recovery...", "#FBBF24");
|
||||||
|
cmd = "reboot recovery";
|
||||||
|
break;
|
||||||
|
case "bootloader":
|
||||||
|
case "fastboot":
|
||||||
|
appendColored("Rebooting to bootloader...", "#FBBF24");
|
||||||
|
cmd = "reboot bootloader";
|
||||||
|
break;
|
||||||
|
case "download":
|
||||||
|
appendColored("Rebooting to download mode...", "#FBBF24");
|
||||||
|
cmd = "reboot download";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
appendColored("Rebooting device...", "#FBBF24");
|
||||||
|
cmd = "reboot";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
executeAsync(cmd, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeAsync(String command, boolean useRoot) {
|
||||||
|
RootShell.executeAsync(command, useRoot, new RootShell.CommandCallback() {
|
||||||
|
@Override
|
||||||
|
public void onOutput(String output) {
|
||||||
|
mainHandler.post(() -> appendOutput(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String error) {
|
||||||
|
mainHandler.post(() -> appendColored(error, "#EF4444"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete(int exitCode) {
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
if (exitCode != 0 && exitCode != -1) {
|
||||||
|
appendColored("Exit code: " + exitCode, "#FBBF24");
|
||||||
|
}
|
||||||
|
appendOutput("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUptimeString() {
|
||||||
|
long uptimeMs = android.os.SystemClock.elapsedRealtime();
|
||||||
|
long hours = uptimeMs / 3600000;
|
||||||
|
long mins = (uptimeMs % 3600000) / 60000;
|
||||||
|
return hours + "h " + mins + "m";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendOutput(String text) {
|
||||||
|
outputView.append(text + "\n");
|
||||||
|
scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendColored(String text, String hexColor) {
|
||||||
|
int start = outputView.getText().length();
|
||||||
|
outputView.append(text + "\n");
|
||||||
|
// Note: For full color support, would need SpannableString
|
||||||
|
// This is simplified - the text color is set by the view
|
||||||
|
scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
510
android/app/src/main/java/com/aethex/os/ThemeManager.java
Normal file
510
android/app/src/main/java/com/aethex/os/ThemeManager.java
Normal file
|
|
@ -0,0 +1,510 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
|
||||||
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
|
|
||||||
|
public class ThemeManager {
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "aethex_prefs";
|
||||||
|
private static final String KEY_CLEARANCE = "clearance_mode";
|
||||||
|
private static final String KEY_ACCENT_COLOR = "accent_color";
|
||||||
|
private static final String KEY_WALLPAPER = "wallpaper_id";
|
||||||
|
private static final String KEY_TRANSPARENCY = "transparency";
|
||||||
|
private static final String KEY_KONAMI_UNLOCKED = "konami_unlocked";
|
||||||
|
private static final String KEY_SOUND_ENABLED = "sound_enabled";
|
||||||
|
|
||||||
|
public static final String CLEARANCE_FOUNDATION = "foundation";
|
||||||
|
public static final String CLEARANCE_CORP = "corp";
|
||||||
|
|
||||||
|
// Wallpaper IDs
|
||||||
|
public static final String WALLPAPER_DEFAULT = "default";
|
||||||
|
public static final String WALLPAPER_CYBER_GRID = "cyber_grid";
|
||||||
|
public static final String WALLPAPER_MATRIX = "matrix";
|
||||||
|
public static final String WALLPAPER_NEON_SUNSET = "neon_sunset";
|
||||||
|
public static final String WALLPAPER_DEEP_OCEAN = "deep_ocean";
|
||||||
|
public static final String WALLPAPER_VAPORWAVE = "vaporwave";
|
||||||
|
public static final String WALLPAPER_BLOOD_MOON = "blood_moon";
|
||||||
|
public static final String WALLPAPER_GALAXY = "galaxy";
|
||||||
|
|
||||||
|
// Accent color options
|
||||||
|
public static final String[][] ACCENT_COLORS = {
|
||||||
|
{"Cyan", "#06B6D4"},
|
||||||
|
{"Purple", "#A855F7"},
|
||||||
|
{"Green", "#22C55E"},
|
||||||
|
{"Orange", "#F97316"},
|
||||||
|
{"Pink", "#EC4899"},
|
||||||
|
{"Red", "#EF4444"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wallpaper definitions: {id, name, secret}
|
||||||
|
public static final String[][] WALLPAPERS = {
|
||||||
|
{WALLPAPER_DEFAULT, "Default", "false"},
|
||||||
|
{WALLPAPER_CYBER_GRID, "Cyber Grid", "false"},
|
||||||
|
{WALLPAPER_MATRIX, "Matrix", "false"},
|
||||||
|
{WALLPAPER_NEON_SUNSET, "Neon Sunset", "false"},
|
||||||
|
{WALLPAPER_DEEP_OCEAN, "Deep Ocean", "false"},
|
||||||
|
{WALLPAPER_VAPORWAVE, "⚡ Vaporwave", "true"},
|
||||||
|
{WALLPAPER_BLOOD_MOON, "🔥 Blood Moon", "true"},
|
||||||
|
{WALLPAPER_GALAXY, "🌌 Galaxy", "true"},
|
||||||
|
};
|
||||||
|
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
|
||||||
|
// Cached typefaces
|
||||||
|
private static Typeface cachedDisplayFont;
|
||||||
|
private static Typeface cachedMonoFont;
|
||||||
|
|
||||||
|
public ThemeManager(Context context) {
|
||||||
|
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Clearance mode ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public String getClearanceMode() {
|
||||||
|
return prefs.getString(KEY_CLEARANCE, CLEARANCE_FOUNDATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClearanceMode(String mode) {
|
||||||
|
prefs.edit().putString(KEY_CLEARANCE, mode).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFoundation() {
|
||||||
|
return CLEARANCE_FOUNDATION.equals(getClearanceMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Accent color ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the user-chosen custom accent color hex string, or null if using theme default.
|
||||||
|
*/
|
||||||
|
public String getCustomAccentColorHex() {
|
||||||
|
return prefs.getString(KEY_ACCENT_COLOR, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom accent color override. Pass null to reset to theme default.
|
||||||
|
*/
|
||||||
|
public void setCustomAccentColor(String hex) {
|
||||||
|
if (hex == null) {
|
||||||
|
prefs.edit().remove(KEY_ACCENT_COLOR).apply();
|
||||||
|
} else {
|
||||||
|
prefs.edit().putString(KEY_ACCENT_COLOR, hex).apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the effective accent color — custom override if set, otherwise theme accent.
|
||||||
|
*/
|
||||||
|
public int getEffectiveAccentColor(Context context) {
|
||||||
|
String custom = getCustomAccentColorHex();
|
||||||
|
if (custom != null) {
|
||||||
|
try {
|
||||||
|
return Color.parseColor(custom);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Fall through to default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getAccentColor(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Wallpaper ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public String getWallpaperId() {
|
||||||
|
return prefs.getString(KEY_WALLPAPER, WALLPAPER_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWallpaperId(String id) {
|
||||||
|
prefs.edit().putString(KEY_WALLPAPER, id).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Transparency ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns panel transparency percentage (50-100). Default 80.
|
||||||
|
* 50 = very transparent, 100 = fully opaque.
|
||||||
|
*/
|
||||||
|
public int getTransparency() {
|
||||||
|
return prefs.getInt(KEY_TRANSPARENCY, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTransparency(int percent) {
|
||||||
|
prefs.edit().putInt(KEY_TRANSPARENCY, Math.max(50, Math.min(100, percent))).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Konami code ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public boolean isKonamiUnlocked() {
|
||||||
|
return prefs.getBoolean(KEY_KONAMI_UNLOCKED, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKonamiUnlocked(boolean unlocked) {
|
||||||
|
prefs.edit().putBoolean(KEY_KONAMI_UNLOCKED, unlocked).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sound ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public boolean isSoundEnabled() {
|
||||||
|
return prefs.getBoolean(KEY_SOUND_ENABLED, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSoundEnabled(boolean enabled) {
|
||||||
|
prefs.edit().putBoolean(KEY_SOUND_ENABLED, enabled).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Color accessors ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public int getPrimaryColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.foundation_primary, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.corp_primary, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAccentColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.foundation_accent, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.corp_accent, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBackgroundColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.foundation_background, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.corp_background, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSurfaceColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.foundation_surface, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.corp_surface, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTextColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.foundation_text, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.corp_text, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSecondaryTextColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.foundation_text_secondary, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.corp_text_secondary, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTaskbarColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.foundation_taskbar, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.corp_taskbar, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBorderColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.foundation_border, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.corp_border, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBootTextColor(Context context) {
|
||||||
|
if (isFoundation()) {
|
||||||
|
return context.getResources().getColor(R.color.boot_text_foundation, context.getTheme());
|
||||||
|
}
|
||||||
|
return context.getResources().getColor(R.color.boot_text_corp, context.getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drawable resource accessors ───────────────────────────────────────
|
||||||
|
|
||||||
|
public int getGlassPanelDrawable() {
|
||||||
|
return isFoundation() ? R.drawable.bg_glass_panel_foundation : R.drawable.bg_glass_panel_corp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getGlassPanelStrongDrawable() {
|
||||||
|
return isFoundation() ? R.drawable.bg_glass_panel_strong_foundation : R.drawable.bg_glass_panel_strong_corp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCardSurfaceDrawable() {
|
||||||
|
return isFoundation() ? R.drawable.bg_card_surface_foundation : R.drawable.bg_card_surface_corp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTaskbarDrawable() {
|
||||||
|
return isFoundation() ? R.drawable.bg_taskbar_foundation : R.drawable.bg_taskbar_corp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBootGlowDrawable() {
|
||||||
|
return isFoundation() ? R.drawable.bg_boot_glow_foundation : R.drawable.bg_boot_glow_corp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBootProgressDrawable() {
|
||||||
|
return isFoundation() ? R.drawable.bg_progress_foundation : R.drawable.bg_progress_corp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBootLogoDrawable() {
|
||||||
|
return isFoundation() ? R.drawable.bg_boot_logo_foundation : R.drawable.bg_boot_logo_corp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSearchInputDrawable() {
|
||||||
|
return isFoundation() ? R.drawable.bg_search_input_foundation : R.drawable.bg_search_input_corp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Wallpaper gradient (radial, built programmatically) ───────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a wallpaper drawable based on user selection or theme default.
|
||||||
|
* If a custom wallpaper is selected, creates that gradient.
|
||||||
|
* Otherwise falls back to the clearance-themed radial gradient.
|
||||||
|
*/
|
||||||
|
public GradientDrawable createWallpaperDrawable(Context context) {
|
||||||
|
String wallpaperId = getWallpaperId();
|
||||||
|
|
||||||
|
if (!WALLPAPER_DEFAULT.equals(wallpaperId)) {
|
||||||
|
return createNamedWallpaper(wallpaperId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: clearance-themed radial gradient
|
||||||
|
return createDefaultWallpaper(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GradientDrawable createDefaultWallpaper(Context context) {
|
||||||
|
int[] colors;
|
||||||
|
if (isFoundation()) {
|
||||||
|
colors = new int[]{
|
||||||
|
context.getResources().getColor(R.color.foundation_wallpaper_start, context.getTheme()),
|
||||||
|
context.getResources().getColor(R.color.foundation_wallpaper_mid, context.getTheme()),
|
||||||
|
context.getResources().getColor(R.color.foundation_wallpaper_end, context.getTheme())
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
colors = new int[]{
|
||||||
|
context.getResources().getColor(R.color.corp_wallpaper_start, context.getTheme()),
|
||||||
|
context.getResources().getColor(R.color.corp_wallpaper_mid, context.getTheme()),
|
||||||
|
context.getResources().getColor(R.color.corp_wallpaper_end, context.getTheme())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientDrawable wallpaper = new GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.TL_BR, colors);
|
||||||
|
wallpaper.setGradientType(GradientDrawable.RADIAL_GRADIENT);
|
||||||
|
wallpaper.setGradientRadius(1200f);
|
||||||
|
|
||||||
|
if (isFoundation()) {
|
||||||
|
wallpaper.setGradientCenter(0.3f, 0.2f);
|
||||||
|
} else {
|
||||||
|
wallpaper.setGradientCenter(0.7f, 0.8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallpaper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GradientDrawable createNamedWallpaper(String id) {
|
||||||
|
int[] colors;
|
||||||
|
GradientDrawable.Orientation orientation;
|
||||||
|
boolean radial = false;
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case WALLPAPER_CYBER_GRID:
|
||||||
|
colors = new int[]{
|
||||||
|
Color.parseColor("#0f172a"),
|
||||||
|
Color.parseColor("#1e1b4b"),
|
||||||
|
Color.parseColor("#0f172a")
|
||||||
|
};
|
||||||
|
orientation = GradientDrawable.Orientation.BL_TR;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WALLPAPER_MATRIX:
|
||||||
|
colors = new int[]{
|
||||||
|
Color.parseColor("#001100"),
|
||||||
|
Color.parseColor("#002200"),
|
||||||
|
Color.parseColor("#001100")
|
||||||
|
};
|
||||||
|
orientation = GradientDrawable.Orientation.TOP_BOTTOM;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WALLPAPER_NEON_SUNSET:
|
||||||
|
colors = new int[]{
|
||||||
|
Color.parseColor("#1a0533"),
|
||||||
|
Color.parseColor("#4a1942"),
|
||||||
|
Color.parseColor("#0f172a")
|
||||||
|
};
|
||||||
|
orientation = GradientDrawable.Orientation.TOP_BOTTOM;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WALLPAPER_DEEP_OCEAN:
|
||||||
|
colors = new int[]{
|
||||||
|
Color.parseColor("#0a1628"),
|
||||||
|
Color.parseColor("#0d3b66"),
|
||||||
|
Color.parseColor("#0a1628")
|
||||||
|
};
|
||||||
|
orientation = GradientDrawable.Orientation.TOP_BOTTOM;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WALLPAPER_VAPORWAVE:
|
||||||
|
colors = new int[]{
|
||||||
|
Color.parseColor("#ff71ce"),
|
||||||
|
Color.parseColor("#01cdfe"),
|
||||||
|
Color.parseColor("#05ffa1"),
|
||||||
|
Color.parseColor("#b967ff")
|
||||||
|
};
|
||||||
|
orientation = GradientDrawable.Orientation.TL_BR;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WALLPAPER_BLOOD_MOON:
|
||||||
|
colors = new int[]{
|
||||||
|
Color.parseColor("#1a0000"),
|
||||||
|
Color.parseColor("#4a0000"),
|
||||||
|
Color.parseColor("#1a0000")
|
||||||
|
};
|
||||||
|
orientation = GradientDrawable.Orientation.TOP_BOTTOM;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WALLPAPER_GALAXY:
|
||||||
|
colors = new int[]{
|
||||||
|
Color.parseColor("#1b2735"),
|
||||||
|
Color.parseColor("#090a0f")
|
||||||
|
};
|
||||||
|
orientation = GradientDrawable.Orientation.TL_BR;
|
||||||
|
radial = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown wallpaper, fall back to dark
|
||||||
|
colors = new int[]{Color.BLACK, Color.BLACK};
|
||||||
|
orientation = GradientDrawable.Orientation.TOP_BOTTOM;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientDrawable gd = new GradientDrawable(orientation, colors);
|
||||||
|
if (radial) {
|
||||||
|
gd.setGradientType(GradientDrawable.RADIAL_GRADIENT);
|
||||||
|
gd.setGradientRadius(1200f);
|
||||||
|
gd.setGradientCenter(0.5f, 0.5f);
|
||||||
|
}
|
||||||
|
return gd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a wallpaper preview drawable for the settings picker.
|
||||||
|
*/
|
||||||
|
public static GradientDrawable createWallpaperPreview(String wallpaperId) {
|
||||||
|
switch (wallpaperId) {
|
||||||
|
case WALLPAPER_CYBER_GRID:
|
||||||
|
return new GradientDrawable(GradientDrawable.Orientation.BL_TR,
|
||||||
|
new int[]{Color.parseColor("#0f172a"), Color.parseColor("#1e1b4b"), Color.parseColor("#0f172a")});
|
||||||
|
case WALLPAPER_MATRIX:
|
||||||
|
return new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
|
||||||
|
new int[]{Color.parseColor("#001100"), Color.parseColor("#002200"), Color.parseColor("#001100")});
|
||||||
|
case WALLPAPER_NEON_SUNSET:
|
||||||
|
return new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
|
||||||
|
new int[]{Color.parseColor("#1a0533"), Color.parseColor("#4a1942"), Color.parseColor("#0f172a")});
|
||||||
|
case WALLPAPER_DEEP_OCEAN:
|
||||||
|
return new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
|
||||||
|
new int[]{Color.parseColor("#0a1628"), Color.parseColor("#0d3b66"), Color.parseColor("#0a1628")});
|
||||||
|
case WALLPAPER_VAPORWAVE:
|
||||||
|
return new GradientDrawable(GradientDrawable.Orientation.TL_BR,
|
||||||
|
new int[]{Color.parseColor("#ff71ce"), Color.parseColor("#01cdfe"), Color.parseColor("#05ffa1"), Color.parseColor("#b967ff")});
|
||||||
|
case WALLPAPER_BLOOD_MOON:
|
||||||
|
return new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
|
||||||
|
new int[]{Color.parseColor("#1a0000"), Color.parseColor("#4a0000"), Color.parseColor("#1a0000")});
|
||||||
|
case WALLPAPER_GALAXY:
|
||||||
|
GradientDrawable gd = new GradientDrawable(GradientDrawable.Orientation.TL_BR,
|
||||||
|
new int[]{Color.parseColor("#1b2735"), Color.parseColor("#090a0f")});
|
||||||
|
gd.setGradientType(GradientDrawable.RADIAL_GRADIENT);
|
||||||
|
gd.setGradientRadius(400f);
|
||||||
|
gd.setGradientCenter(0.5f, 0.5f);
|
||||||
|
return gd;
|
||||||
|
default:
|
||||||
|
return new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
|
||||||
|
new int[]{Color.parseColor("#0A0202"), Color.parseColor("#0A0202")});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Start button gradient ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
public GradientDrawable createStartButtonDrawable() {
|
||||||
|
int[] colors;
|
||||||
|
if (isFoundation()) {
|
||||||
|
colors = new int[]{ Color.parseColor("#DC2626"), Color.parseColor("#D4AF37") };
|
||||||
|
} else {
|
||||||
|
colors = new int[]{ Color.parseColor("#3B82F6"), Color.parseColor("#06B6D4") };
|
||||||
|
}
|
||||||
|
GradientDrawable gd = new GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.LEFT_RIGHT, colors);
|
||||||
|
gd.setCornerRadius(dpToPx(10));
|
||||||
|
return gd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Clearance badge colors ────────────────────────────────────────────
|
||||||
|
|
||||||
|
public int getClearanceBadgeBackground() {
|
||||||
|
return isFoundation()
|
||||||
|
? Color.parseColor("#33DC2626")
|
||||||
|
: Color.parseColor("#333B82F6");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getClearanceBadgeTextColor() {
|
||||||
|
return isFoundation()
|
||||||
|
? Color.parseColor("#D4AF37")
|
||||||
|
: Color.parseColor("#C0C0C0");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClearanceLabel() {
|
||||||
|
return isFoundation() ? "FOUNDATION CLEARANCE" : "CORP CLEARANCE";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Clearance switching animation colors ──────────────────────────────
|
||||||
|
|
||||||
|
public static int getSwitchOverlayBackground(String targetMode) {
|
||||||
|
return CLEARANCE_CORP.equals(targetMode)
|
||||||
|
? Color.parseColor("#0F172A")
|
||||||
|
: Color.parseColor("#1a0505");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSwitchSpinnerColor(String targetMode) {
|
||||||
|
return CLEARANCE_CORP.equals(targetMode)
|
||||||
|
? Color.parseColor("#3B82F6")
|
||||||
|
: Color.parseColor("#D4AF37");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSwitchLabel(String targetMode) {
|
||||||
|
return CLEARANCE_CORP.equals(targetMode)
|
||||||
|
? "ENTERING CORP"
|
||||||
|
: "ENTERING FOUNDATION";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Font accessors ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public Typeface getDisplayFont(Context context) {
|
||||||
|
if (cachedDisplayFont == null) {
|
||||||
|
try {
|
||||||
|
cachedDisplayFont = ResourcesCompat.getFont(context, R.font.electrolize);
|
||||||
|
} catch (Exception e) {
|
||||||
|
cachedDisplayFont = Typeface.SANS_SERIF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cachedDisplayFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Typeface getMonoFont(Context context) {
|
||||||
|
if (cachedMonoFont == null) {
|
||||||
|
try {
|
||||||
|
cachedMonoFont = ResourcesCompat.getFont(context, R.font.source_code_pro);
|
||||||
|
} catch (Exception e) {
|
||||||
|
cachedMonoFont = Typeface.MONOSPACE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cachedMonoFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Utility ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static float dpToPx(int dp) {
|
||||||
|
return dp * android.content.res.Resources.getSystem().getDisplayMetrics().density;
|
||||||
|
}
|
||||||
|
}
|
||||||
482
android/app/src/main/java/com/aethex/os/WeatherActivity.java
Normal file
482
android/app/src/main/java/com/aethex/os/WeatherActivity.java
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class WeatherActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ThemeManager themeManager;
|
||||||
|
|
||||||
|
private static final String[] CONDITIONS = {
|
||||||
|
"Clear Sky", "Partly Cloudy", "Overcast", "Light Rain",
|
||||||
|
"Scattered Clouds", "Foggy", "Windy", "Thunderstorms"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current weather state (stored for F/C toggling and refresh)
|
||||||
|
private int currentTempF;
|
||||||
|
private int currentHiF;
|
||||||
|
private int currentLoF;
|
||||||
|
private String currentCondition;
|
||||||
|
private boolean isCelsius = false;
|
||||||
|
|
||||||
|
// Hourly temps stored for F/C toggling
|
||||||
|
private int[] hourlyTempsF = new int[12];
|
||||||
|
private String[] hourlyLabels = new String[12];
|
||||||
|
private String[] hourlyEmojis = new String[12];
|
||||||
|
|
||||||
|
// Forecast data stored for F/C toggling and tap info
|
||||||
|
private int[] forecastHiF = new int[5];
|
||||||
|
private int[] forecastLoF = new int[5];
|
||||||
|
private String[] forecastConditions = new String[5];
|
||||||
|
private String[] forecastDayNames = new String[5];
|
||||||
|
private String[] forecastEmojis = new String[5];
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_weather);
|
||||||
|
hideSystemUI();
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(this);
|
||||||
|
|
||||||
|
// Play open sound
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.OPEN);
|
||||||
|
|
||||||
|
// Track achievement stat
|
||||||
|
AchievementsActivity.incrementStat(this, "weather_checks");
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
findViewById(R.id.weather_back).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLOSE);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(R.anim.scale_in, R.anim.slide_down_out);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
findViewById(R.id.weather_refresh).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
refreshAllData();
|
||||||
|
AeThexToast.show(WeatherActivity.this, "Weather updated", AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Temperature tap to toggle F/C
|
||||||
|
findViewById(R.id.weather_temp).setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
isCelsius = !isCelsius;
|
||||||
|
updateTemperatureDisplay();
|
||||||
|
updateHourlyDisplay();
|
||||||
|
updateForecastDisplay();
|
||||||
|
String unit = isCelsius ? "Celsius" : "Fahrenheit";
|
||||||
|
AeThexToast.show(WeatherActivity.this, "Switched to " + unit, AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
generateWeatherData();
|
||||||
|
buildHourlyForecast();
|
||||||
|
buildForecast();
|
||||||
|
|
||||||
|
// Entrance animation
|
||||||
|
View root = findViewById(R.id.weather_root);
|
||||||
|
root.setAlpha(0f);
|
||||||
|
root.animate().alpha(1f).setDuration(300).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh all weather data with new random values.
|
||||||
|
*/
|
||||||
|
private void refreshAllData() {
|
||||||
|
generateWeatherData();
|
||||||
|
|
||||||
|
// Rebuild hourly forecast
|
||||||
|
LinearLayout hourlyContainer = findViewById(R.id.hourly_container);
|
||||||
|
hourlyContainer.removeAllViews();
|
||||||
|
buildHourlyForecast();
|
||||||
|
|
||||||
|
// Rebuild 5-day forecast
|
||||||
|
LinearLayout forecastContainer = findViewById(R.id.forecast_container);
|
||||||
|
forecastContainer.removeAllViews();
|
||||||
|
buildForecast();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the emoji for a given weather condition string.
|
||||||
|
*/
|
||||||
|
private String getConditionEmoji(String condition) {
|
||||||
|
switch (condition) {
|
||||||
|
case "Clear Sky": return "\u2600\uFE0F"; // sun
|
||||||
|
case "Partly Cloudy": return "\u26C5"; // sun behind cloud
|
||||||
|
case "Overcast": return "\u2601\uFE0F"; // cloud
|
||||||
|
case "Light Rain": return "\uD83C\uDF27\uFE0F"; // cloud with rain
|
||||||
|
case "Scattered Clouds": return "\uD83C\uDF24\uFE0F"; // sun behind small cloud
|
||||||
|
case "Foggy": return "\uD83C\uDF2B\uFE0F"; // fog
|
||||||
|
case "Windy": return "\uD83C\uDF2C\uFE0F"; // wind face
|
||||||
|
case "Thunderstorms": return "\u26C8\uFE0F"; // cloud with lightning and rain
|
||||||
|
default: return "\uD83C\uDF21\uFE0F"; // thermometer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Fahrenheit to Celsius.
|
||||||
|
*/
|
||||||
|
private int toCelsius(int fahrenheit) {
|
||||||
|
return Math.round((fahrenheit - 32) * 5f / 9f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format temperature with current unit suffix.
|
||||||
|
*/
|
||||||
|
private String formatTemp(int fahrenheit) {
|
||||||
|
if (isCelsius) {
|
||||||
|
return toCelsius(fahrenheit) + "\u00B0C";
|
||||||
|
}
|
||||||
|
return fahrenheit + "\u00B0F";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format temperature for the big display (no unit letter, just degree symbol).
|
||||||
|
*/
|
||||||
|
private String formatTempBig(int fahrenheit) {
|
||||||
|
if (isCelsius) {
|
||||||
|
return toCelsius(fahrenheit) + "\u00B0";
|
||||||
|
}
|
||||||
|
return fahrenheit + "\u00B0";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateWeatherData() {
|
||||||
|
Random rand = new Random();
|
||||||
|
currentTempF = 60 + rand.nextInt(30); // 60-89
|
||||||
|
currentHiF = currentTempF + rand.nextInt(8) + 2;
|
||||||
|
currentLoF = currentTempF - rand.nextInt(8) - 2;
|
||||||
|
currentCondition = CONDITIONS[rand.nextInt(CONDITIONS.length)];
|
||||||
|
int wind = 3 + rand.nextInt(20);
|
||||||
|
int humidity = 30 + rand.nextInt(50);
|
||||||
|
float pressure = 29.5f + rand.nextFloat() * 1.5f;
|
||||||
|
int uv = rand.nextInt(11);
|
||||||
|
|
||||||
|
String uvLabel;
|
||||||
|
if (uv <= 2) uvLabel = uv + " Low";
|
||||||
|
else if (uv <= 5) uvLabel = uv + " Moderate";
|
||||||
|
else if (uv <= 7) uvLabel = uv + " High";
|
||||||
|
else uvLabel = uv + " Very High";
|
||||||
|
|
||||||
|
// Generate sunrise/sunset times
|
||||||
|
int sunriseHour = 5 + rand.nextInt(2); // 5 or 6
|
||||||
|
int sunriseMin = rand.nextInt(60);
|
||||||
|
int sunsetHour = 17 + rand.nextInt(3); // 5-7 PM (17-19)
|
||||||
|
int sunsetMin = rand.nextInt(60);
|
||||||
|
String sunrise = String.format(Locale.US, "%d:%02d AM", sunriseHour, sunriseMin);
|
||||||
|
String sunset = String.format(Locale.US, "%d:%02d PM", sunsetHour - 12, sunsetMin);
|
||||||
|
|
||||||
|
// Set location with current date
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("EEEE, MMM d", Locale.getDefault());
|
||||||
|
TextView location = findViewById(R.id.weather_location);
|
||||||
|
location.setText("AeThex HQ \u2022 " + sdf.format(Calendar.getInstance().getTime()));
|
||||||
|
location.setTypeface(themeManager.getMonoFont(this));
|
||||||
|
|
||||||
|
// Weather emoji
|
||||||
|
String emoji = getConditionEmoji(currentCondition);
|
||||||
|
((TextView) findViewById(R.id.weather_emoji)).setText(emoji);
|
||||||
|
|
||||||
|
// Big temperature
|
||||||
|
TextView tempView = findViewById(R.id.weather_temp);
|
||||||
|
tempView.setText(formatTempBig(currentTempF));
|
||||||
|
tempView.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
|
||||||
|
// Condition with emoji
|
||||||
|
TextView condView = findViewById(R.id.weather_condition);
|
||||||
|
condView.setText(emoji + " " + currentCondition);
|
||||||
|
condView.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
condView.setTextColor(themeManager.getPrimaryColor(this));
|
||||||
|
|
||||||
|
// Hi / Lo
|
||||||
|
TextView hiloView = findViewById(R.id.weather_hilo);
|
||||||
|
hiloView.setTypeface(themeManager.getMonoFont(this));
|
||||||
|
updateHiLoText(hiloView);
|
||||||
|
|
||||||
|
// Detail stats
|
||||||
|
TextView windView = findViewById(R.id.weather_wind);
|
||||||
|
windView.setText(wind + " mph");
|
||||||
|
windView.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
|
||||||
|
TextView humidityView = findViewById(R.id.weather_humidity);
|
||||||
|
humidityView.setText(humidity + "%");
|
||||||
|
humidityView.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
|
||||||
|
TextView pressureView = findViewById(R.id.weather_pressure);
|
||||||
|
pressureView.setText(String.format(Locale.US, "%.1f in", pressure));
|
||||||
|
pressureView.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
|
||||||
|
TextView uvView = findViewById(R.id.weather_uv);
|
||||||
|
uvView.setText(uvLabel);
|
||||||
|
uvView.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
|
||||||
|
// Sunrise / Sunset
|
||||||
|
TextView sunriseView = findViewById(R.id.weather_sunrise);
|
||||||
|
sunriseView.setText("\u2600\uFE0F " + sunrise);
|
||||||
|
sunriseView.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
|
||||||
|
TextView sunsetView = findViewById(R.id.weather_sunset);
|
||||||
|
sunsetView.setText("\uD83C\uDF05 " + sunset);
|
||||||
|
sunsetView.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHiLoText(TextView hiloView) {
|
||||||
|
if (isCelsius) {
|
||||||
|
hiloView.setText("H: " + toCelsius(currentHiF) + "\u00B0C L: " + toCelsius(currentLoF) + "\u00B0C");
|
||||||
|
} else {
|
||||||
|
hiloView.setText("H: " + currentHiF + "\u00B0F L: " + currentLoF + "\u00B0F");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update temperature display when toggling F/C (main temp + hi/lo).
|
||||||
|
*/
|
||||||
|
private void updateTemperatureDisplay() {
|
||||||
|
TextView tempView = findViewById(R.id.weather_temp);
|
||||||
|
tempView.setText(formatTempBig(currentTempF));
|
||||||
|
|
||||||
|
TextView hiloView = findViewById(R.id.weather_hilo);
|
||||||
|
updateHiLoText(hiloView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the hourly forecast (next 12 hours).
|
||||||
|
*/
|
||||||
|
private void buildHourlyForecast() {
|
||||||
|
LinearLayout container = findViewById(R.id.hourly_container);
|
||||||
|
Random rand = new Random();
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
cal.add(Calendar.HOUR_OF_DAY, 1);
|
||||||
|
int hour = cal.get(Calendar.HOUR_OF_DAY);
|
||||||
|
String amPm = hour >= 12 ? "PM" : "AM";
|
||||||
|
int displayHour = hour % 12;
|
||||||
|
if (displayHour == 0) displayHour = 12;
|
||||||
|
hourlyLabels[i] = displayHour + amPm;
|
||||||
|
|
||||||
|
// Hourly temp varies around current temp
|
||||||
|
hourlyTempsF[i] = currentTempF + rand.nextInt(11) - 5; // +/- 5 degrees
|
||||||
|
String hourCondition = CONDITIONS[rand.nextInt(CONDITIONS.length)];
|
||||||
|
hourlyEmojis[i] = getConditionEmoji(hourCondition);
|
||||||
|
|
||||||
|
container.addView(createHourlyItem(i));
|
||||||
|
|
||||||
|
// Spacer between items
|
||||||
|
if (i < 11) {
|
||||||
|
View spacer = new View(this);
|
||||||
|
spacer.setMinimumWidth(dp(2));
|
||||||
|
container.addView(spacer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single hourly forecast item view.
|
||||||
|
*/
|
||||||
|
private LinearLayout createHourlyItem(int index) {
|
||||||
|
LinearLayout item = new LinearLayout(this);
|
||||||
|
item.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
item.setGravity(Gravity.CENTER);
|
||||||
|
item.setPadding(dp(12), dp(8), dp(12), dp(8));
|
||||||
|
|
||||||
|
// Hour label
|
||||||
|
TextView hourText = new TextView(this);
|
||||||
|
hourText.setText(hourlyLabels[index]);
|
||||||
|
hourText.setTextSize(11);
|
||||||
|
hourText.setTextColor(0x99FFFFFF);
|
||||||
|
hourText.setTypeface(themeManager.getMonoFont(this));
|
||||||
|
hourText.setGravity(Gravity.CENTER);
|
||||||
|
item.addView(hourText);
|
||||||
|
|
||||||
|
// Emoji
|
||||||
|
TextView emojiText = new TextView(this);
|
||||||
|
emojiText.setText(hourlyEmojis[index]);
|
||||||
|
emojiText.setTextSize(20);
|
||||||
|
emojiText.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams emojiParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
emojiParams.topMargin = dp(4);
|
||||||
|
emojiParams.bottomMargin = dp(4);
|
||||||
|
emojiText.setLayoutParams(emojiParams);
|
||||||
|
item.addView(emojiText);
|
||||||
|
|
||||||
|
// Temperature
|
||||||
|
TextView tempText = new TextView(this);
|
||||||
|
tempText.setTag("hourly_temp_" + index);
|
||||||
|
tempText.setText(formatTemp(hourlyTempsF[index]));
|
||||||
|
tempText.setTextSize(13);
|
||||||
|
tempText.setTextColor(0xFFFFFFFF);
|
||||||
|
tempText.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
tempText.setGravity(Gravity.CENTER);
|
||||||
|
item.addView(tempText);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all hourly forecast temp labels when toggling F/C.
|
||||||
|
*/
|
||||||
|
private void updateHourlyDisplay() {
|
||||||
|
LinearLayout container = findViewById(R.id.hourly_container);
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
TextView tv = container.findViewWithTag("hourly_temp_" + i);
|
||||||
|
if (tv != null) {
|
||||||
|
tv.setText(formatTemp(hourlyTempsF[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildForecast() {
|
||||||
|
LinearLayout container = findViewById(R.id.forecast_container);
|
||||||
|
Random rand = new Random();
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
cal.add(Calendar.DAY_OF_YEAR, 1);
|
||||||
|
forecastDayNames[i] = new SimpleDateFormat("EEE", Locale.getDefault()).format(cal.getTime()).toUpperCase();
|
||||||
|
forecastHiF[i] = 65 + rand.nextInt(25);
|
||||||
|
forecastLoF[i] = forecastHiF[i] - 5 - rand.nextInt(10);
|
||||||
|
forecastConditions[i] = CONDITIONS[rand.nextInt(CONDITIONS.length)];
|
||||||
|
forecastEmojis[i] = getConditionEmoji(forecastConditions[i]);
|
||||||
|
|
||||||
|
container.addView(createForecastRow(i));
|
||||||
|
|
||||||
|
// Add divider between rows (not after last)
|
||||||
|
if (i < 4) {
|
||||||
|
View divider = new View(this);
|
||||||
|
divider.setBackgroundColor(0x1AFFFFFF);
|
||||||
|
LinearLayout.LayoutParams divParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, 1);
|
||||||
|
divParams.setMarginStart(dp(16));
|
||||||
|
divParams.setMarginEnd(dp(16));
|
||||||
|
divider.setLayoutParams(divParams);
|
||||||
|
container.addView(divider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single forecast row.
|
||||||
|
*/
|
||||||
|
private LinearLayout createForecastRow(int index) {
|
||||||
|
LinearLayout row = new LinearLayout(this);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
row.setPadding(dp(16), dp(14), dp(16), dp(14));
|
||||||
|
row.setTag("forecast_row_" + index);
|
||||||
|
|
||||||
|
// Day name
|
||||||
|
TextView dayText = new TextView(this);
|
||||||
|
dayText.setText(forecastDayNames[index]);
|
||||||
|
dayText.setTextSize(13);
|
||||||
|
dayText.setTextColor(0xCCFFFFFF);
|
||||||
|
dayText.setTypeface(themeManager.getMonoFont(this));
|
||||||
|
dayText.setWidth(dp(50));
|
||||||
|
row.addView(dayText);
|
||||||
|
|
||||||
|
// Emoji
|
||||||
|
TextView emojiText = new TextView(this);
|
||||||
|
emojiText.setText(forecastEmojis[index]);
|
||||||
|
emojiText.setTextSize(16);
|
||||||
|
emojiText.setWidth(dp(30));
|
||||||
|
row.addView(emojiText);
|
||||||
|
|
||||||
|
// Condition
|
||||||
|
TextView condText = new TextView(this);
|
||||||
|
condText.setTag("forecast_cond_" + index);
|
||||||
|
condText.setText(forecastConditions[index]);
|
||||||
|
condText.setTextSize(12);
|
||||||
|
condText.setTextColor(0x99FFFFFF);
|
||||||
|
condText.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
|
||||||
|
LinearLayout.LayoutParams condParams = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
condText.setLayoutParams(condParams);
|
||||||
|
row.addView(condText);
|
||||||
|
|
||||||
|
// Temps
|
||||||
|
TextView tempText = new TextView(this);
|
||||||
|
tempText.setTag("forecast_temp_" + index);
|
||||||
|
tempText.setText(formatForecastTemp(index));
|
||||||
|
tempText.setTextSize(14);
|
||||||
|
tempText.setTextColor(0xFFFFFFFF);
|
||||||
|
tempText.setTypeface(themeManager.getDisplayFont(this));
|
||||||
|
row.addView(tempText);
|
||||||
|
|
||||||
|
// Tap handler: show full day info toast
|
||||||
|
final int idx = index;
|
||||||
|
row.setOnClickListener(v -> {
|
||||||
|
SoundManager.getInstance().play(SoundManager.Sound.CLICK);
|
||||||
|
String info = forecastDayNames[idx] + ": " + forecastEmojis[idx] + " " + forecastConditions[idx]
|
||||||
|
+ " | High " + formatTemp(forecastHiF[idx])
|
||||||
|
+ " / Low " + formatTemp(forecastLoF[idx]);
|
||||||
|
AeThexToast.show(WeatherActivity.this, info, AeThexToast.Type.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make it look tappable
|
||||||
|
row.setClickable(true);
|
||||||
|
row.setFocusable(true);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format forecast temp string for a given index.
|
||||||
|
*/
|
||||||
|
private String formatForecastTemp(int index) {
|
||||||
|
if (isCelsius) {
|
||||||
|
return toCelsius(forecastLoF[index]) + "\u00B0 / " + toCelsius(forecastHiF[index]) + "\u00B0C";
|
||||||
|
}
|
||||||
|
return forecastLoF[index] + "\u00B0 / " + forecastHiF[index] + "\u00B0F";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all forecast temp labels when toggling F/C.
|
||||||
|
*/
|
||||||
|
private void updateForecastDisplay() {
|
||||||
|
LinearLayout container = findViewById(R.id.forecast_container);
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
TextView tv = container.findViewWithTag("forecast_temp_" + i);
|
||||||
|
if (tv != null) {
|
||||||
|
tv.setText(formatForecastTemp(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
return (int) (value * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) hideSystemUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
final WindowInsetsController c = getWindow().getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
android/app/src/main/res/anim/fade_in.xml
Normal file
6
android/app/src/main/res/anim/fade_in.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:duration="200"
|
||||||
|
android:interpolator="@android:interpolator/decelerate_quad" />
|
||||||
6
android/app/src/main/res/anim/fade_out.xml
Normal file
6
android/app/src/main/res/anim/fade_out.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:duration="200"
|
||||||
|
android:interpolator="@android:interpolator/accelerate_quad" />
|
||||||
16
android/app/src/main/res/anim/scale_in.xml
Normal file
16
android/app/src/main/res/anim/scale_in.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:interpolator="@android:anim/decelerate_interpolator">
|
||||||
|
<scale
|
||||||
|
android:fromXScale="0.92"
|
||||||
|
android:toXScale="1.0"
|
||||||
|
android:fromYScale="0.92"
|
||||||
|
android:toYScale="1.0"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%"
|
||||||
|
android:duration="250" />
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:duration="200" />
|
||||||
|
</set>
|
||||||
16
android/app/src/main/res/anim/scale_out.xml
Normal file
16
android/app/src/main/res/anim/scale_out.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:interpolator="@android:anim/accelerate_interpolator">
|
||||||
|
<scale
|
||||||
|
android:fromXScale="1.0"
|
||||||
|
android:toXScale="0.92"
|
||||||
|
android:fromYScale="1.0"
|
||||||
|
android:toYScale="0.92"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%"
|
||||||
|
android:duration="200" />
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:duration="180" />
|
||||||
|
</set>
|
||||||
12
android/app/src/main/res/anim/slide_down_out.xml
Normal file
12
android/app/src/main/res/anim/slide_down_out.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:interpolator="@android:anim/accelerate_interpolator">
|
||||||
|
<translate
|
||||||
|
android:fromYDelta="0%"
|
||||||
|
android:toYDelta="5%"
|
||||||
|
android:duration="200" />
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:duration="180" />
|
||||||
|
</set>
|
||||||
12
android/app/src/main/res/anim/slide_up_in.xml
Normal file
12
android/app/src/main/res/anim/slide_up_in.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:interpolator="@android:anim/decelerate_interpolator">
|
||||||
|
<translate
|
||||||
|
android:fromYDelta="8%"
|
||||||
|
android:toYDelta="0%"
|
||||||
|
android:duration="250" />
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:duration="200" />
|
||||||
|
</set>
|
||||||
9
android/app/src/main/res/drawable/bg_app_icon_mask.xml
Normal file
9
android/app/src/main/res/drawable/bg_app_icon_mask.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
<solid android:color="#1AFFFFFF" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#0DFFFFFF" />
|
||||||
|
</shape>
|
||||||
15
android/app/src/main/res/drawable/bg_back_button.xml
Normal file
15
android/app/src/main/res/drawable/bg_back_button.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#33FFFFFF" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#1AFFFFFF" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
5
android/app/src/main/res/drawable/bg_bell_badge.xml
Normal file
5
android/app/src/main/res/drawable/bg_bell_badge.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="#EF4444" />
|
||||||
|
</shape>
|
||||||
9
android/app/src/main/res/drawable/bg_boot_glow.xml
Normal file
9
android/app/src/main/res/drawable/bg_boot_glow.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<gradient
|
||||||
|
android:type="radial"
|
||||||
|
android:gradientRadius="120dp"
|
||||||
|
android:centerColor="#4006B6D4"
|
||||||
|
android:endColor="#00000000" />
|
||||||
|
</shape>
|
||||||
11
android/app/src/main/res/drawable/bg_boot_glow_corp.xml
Normal file
11
android/app/src/main/res/drawable/bg_boot_glow_corp.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<gradient
|
||||||
|
android:type="radial"
|
||||||
|
android:gradientRadius="200dp"
|
||||||
|
android:centerX="0.5"
|
||||||
|
android:centerY="0.5"
|
||||||
|
android:startColor="#3306B6D4"
|
||||||
|
android:endColor="#00000000" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<gradient
|
||||||
|
android:type="radial"
|
||||||
|
android:gradientRadius="200dp"
|
||||||
|
android:centerX="0.5"
|
||||||
|
android:centerY="0.5"
|
||||||
|
android:startColor="#33DC2626"
|
||||||
|
android:endColor="#00000000" />
|
||||||
|
</shape>
|
||||||
9
android/app/src/main/res/drawable/bg_boot_logo.xml
Normal file
9
android/app/src/main/res/drawable/bg_boot_logo.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#06B6D4"
|
||||||
|
android:endColor="#A855F7"
|
||||||
|
android:angle="135" />
|
||||||
|
<corners android:radius="24dp" />
|
||||||
|
</shape>
|
||||||
9
android/app/src/main/res/drawable/bg_boot_logo_corp.xml
Normal file
9
android/app/src/main/res/drawable/bg_boot_logo_corp.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#2563EB"
|
||||||
|
android:endColor="#06B6D4"
|
||||||
|
android:angle="135" />
|
||||||
|
<corners android:radius="24dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#DC2626"
|
||||||
|
android:endColor="#F59E0B"
|
||||||
|
android:angle="135" />
|
||||||
|
<corners android:radius="24dp" />
|
||||||
|
</shape>
|
||||||
15
android/app/src/main/res/drawable/bg_browser_progress.xml
Normal file
15
android/app/src/main/res/drawable/bg_browser_progress.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@android:id/background">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#00000000" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<clip>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#06B6D4" />
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
15
android/app/src/main/res/drawable/bg_calc_button.xml
Normal file
15
android/app/src/main/res/drawable/bg_calc_button.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#33FFFFFF" />
|
||||||
|
<corners android:radius="14dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#1AFFFFFF" />
|
||||||
|
<corners android:radius="14dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
21
android/app/src/main/res/drawable/bg_calc_button_equals.xml
Normal file
21
android/app/src/main/res/drawable/bg_calc_button_equals.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#0891B2"
|
||||||
|
android:endColor="#7C3AED"
|
||||||
|
android:angle="135" />
|
||||||
|
<corners android:radius="14dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#06B6D4"
|
||||||
|
android:endColor="#A855F7"
|
||||||
|
android:angle="135" />
|
||||||
|
<corners android:radius="14dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
15
android/app/src/main/res/drawable/bg_calc_button_func.xml
Normal file
15
android/app/src/main/res/drawable/bg_calc_button_func.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#33FF7043" />
|
||||||
|
<corners android:radius="14dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#26FFFFFF" />
|
||||||
|
<corners android:radius="14dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
15
android/app/src/main/res/drawable/bg_calc_button_op.xml
Normal file
15
android/app/src/main/res/drawable/bg_calc_button_op.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#4006B6D4" />
|
||||||
|
<corners android:radius="14dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#2606B6D4" />
|
||||||
|
<corners android:radius="14dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
7
android/app/src/main/res/drawable/bg_card_surface.xml
Normal file
7
android/app/src/main/res/drawable/bg_card_surface.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#1AFFFFFF" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#0DFFFFFF" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#1A3B82F6" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#2694A3B8" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#1ADC2626" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#33CA8A04" />
|
||||||
|
</shape>
|
||||||
6
android/app/src/main/res/drawable/bg_color_swatch.xml
Normal file
6
android/app/src/main/res/drawable/bg_color_swatch.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="#06B6D4" />
|
||||||
|
<size android:width="36dp" android:height="36dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="#06B6D4" />
|
||||||
|
<stroke android:width="2dp" android:color="#FFFFFF" />
|
||||||
|
<size android:width="36dp" android:height="36dp" />
|
||||||
|
</shape>
|
||||||
5
android/app/src/main/res/drawable/bg_dock_dot.xml
Normal file
5
android/app/src/main/res/drawable/bg_dock_dot.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="#80FFFFFF" />
|
||||||
|
</shape>
|
||||||
7
android/app/src/main/res/drawable/bg_glass_panel.xml
Normal file
7
android/app/src/main/res/drawable/bg_glass_panel.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#CC0F172A" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#1AFFFFFF" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#CC0A0A1A" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#4D94A3B8" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#CC1A0A0A" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#66CA8A04" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#E60F172A" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#1AFFFFFF" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#E60A0A1A" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#4D94A3B8" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#E61A0A0A" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#66CA8A04" />
|
||||||
|
</shape>
|
||||||
5
android/app/src/main/res/drawable/bg_icon_card.xml
Normal file
5
android/app/src/main/res/drawable/bg_icon_card.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#00000000" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#0DFFFFFF" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
</shape>
|
||||||
20
android/app/src/main/res/drawable/bg_progress_corp.xml
Normal file
20
android/app/src/main/res/drawable/bg_progress_corp.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@android:id/background">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#1A3B82F6" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<clip>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#3B82F6"
|
||||||
|
android:endColor="#06B6D4"
|
||||||
|
android:angle="0" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
20
android/app/src/main/res/drawable/bg_progress_foundation.xml
Normal file
20
android/app/src/main/res/drawable/bg_progress_foundation.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@android:id/background">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#1ADC2626" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<clip>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#DC2626"
|
||||||
|
android:endColor="#D4AF37"
|
||||||
|
android:angle="0" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
21
android/app/src/main/res/drawable/bg_progress_modern.xml
Normal file
21
android/app/src/main/res/drawable/bg_progress_modern.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@android:id/background">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#1A18181B" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#1AFFFFFF" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<clip>
|
||||||
|
<shape>
|
||||||
|
<gradient
|
||||||
|
android:startColor="#06B6D4"
|
||||||
|
android:endColor="#A855F7"
|
||||||
|
android:angle="0" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
6
android/app/src/main/res/drawable/bg_scanlines.xml
Normal file
6
android/app/src/main/res/drawable/bg_scanlines.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- CRT scanline overlay - applied programmatically via custom View for best effect -->
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#08000000" />
|
||||||
|
</shape>
|
||||||
7
android/app/src/main/res/drawable/bg_search_input.xml
Normal file
7
android/app/src/main/res/drawable/bg_search_input.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#0DFFFFFF" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#1AFFFFFF" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#0DFFFFFF" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#333B82F6" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#0DFFFFFF" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#33D4AF37" />
|
||||||
|
</shape>
|
||||||
17
android/app/src/main/res/drawable/bg_settings_card_corp.xml
Normal file
17
android/app/src/main/res/drawable/bg_settings_card_corp.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_selected="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#332563EB" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="2dp" android:color="#802563EB" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#1AFFFFFF" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#0DFFFFFF" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_selected="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#33DC2626" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="2dp" android:color="#80DC2626" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#1AFFFFFF" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#0DFFFFFF" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
15
android/app/src/main/res/drawable/bg_social_button.xml
Normal file
15
android/app/src/main/res/drawable/bg_social_button.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#33FFFFFF" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#0DFFFFFF" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
9
android/app/src/main/res/drawable/bg_start_button.xml
Normal file
9
android/app/src/main/res/drawable/bg_start_button.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#06B6D4"
|
||||||
|
android:endColor="#A855F7"
|
||||||
|
android:angle="135" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
</shape>
|
||||||
5
android/app/src/main/res/drawable/bg_taskbar.xml
Normal file
5
android/app/src/main/res/drawable/bg_taskbar.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#E60F172A" />
|
||||||
|
</shape>
|
||||||
15
android/app/src/main/res/drawable/bg_taskbar_button.xml
Normal file
15
android/app/src/main/res/drawable/bg_taskbar_button.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#3306B6D4" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#00000000" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
5
android/app/src/main/res/drawable/bg_taskbar_corp.xml
Normal file
5
android/app/src/main/res/drawable/bg_taskbar_corp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#E60F172A" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#E61A0505" />
|
||||||
|
</shape>
|
||||||
9
android/app/src/main/res/drawable/bg_terminal.xml
Normal file
9
android/app/src/main/res/drawable/bg_terminal.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#F5030712"
|
||||||
|
android:endColor="#F5030712"
|
||||||
|
android:angle="270" />
|
||||||
|
<corners android:radius="0dp" />
|
||||||
|
</shape>
|
||||||
7
android/app/src/main/res/drawable/bg_terminal_input.xml
Normal file
7
android/app/src/main/res/drawable/bg_terminal_input.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#1A00FF00" />
|
||||||
|
<corners android:topLeftRadius="0dp" android:topRightRadius="0dp" />
|
||||||
|
<stroke android:width="0dp" android:color="#00000000" />
|
||||||
|
</shape>
|
||||||
8
android/app/src/main/res/drawable/bg_titlebar.xml
Normal file
8
android/app/src/main/res/drawable/bg_titlebar.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#CC334155"
|
||||||
|
android:endColor="#CC0F172A"
|
||||||
|
android:angle="0" />
|
||||||
|
</shape>
|
||||||
7
android/app/src/main/res/drawable/bg_toast.xml
Normal file
7
android/app/src/main/res/drawable/bg_toast.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#E60F172A" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#1AFFFFFF" />
|
||||||
|
</shape>
|
||||||
7
android/app/src/main/res/drawable/bg_wallpaper_card.xml
Normal file
7
android/app/src/main/res/drawable/bg_wallpaper_card.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#00000000" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#33FFFFFF" />
|
||||||
|
</shape>
|
||||||
10
android/app/src/main/res/drawable/bg_window.xml
Normal file
10
android/app/src/main/res/drawable/bg_window.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#FA0F172A"
|
||||||
|
android:endColor="#F50F172A"
|
||||||
|
android:angle="270" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#1AFFFFFF" />
|
||||||
|
</shape>
|
||||||
17
android/app/src/main/res/drawable/boot_progress_drawable.xml
Normal file
17
android/app/src/main/res/drawable/boot_progress_drawable.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@android:id/background">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/boot_progress_track" />
|
||||||
|
<corners android:radius="3dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<clip>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/boot_progress" />
|
||||||
|
<corners android:radius="3dp" />
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue