mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 06:14:35 +03:00
feat: add Android client with service integration, UI, and build configuration
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.gradle/
|
||||
local.properties
|
||||
build/
|
||||
app/build/
|
||||
@@ -0,0 +1,42 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.thefeed.android'
|
||||
compileSdk 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'com.thefeed.android'
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 1
|
||||
versionName '1.0.0'
|
||||
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.15.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.10.1'
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
# Intentionally minimal. The app mostly wraps a local process and WebView.
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@android:drawable/sym_def_app_icon"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@android:drawable/sym_def_app_icon"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Thefeed">
|
||||
|
||||
<service
|
||||
android:name=".ThefeedService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,12 @@
|
||||
Place the Android client binary in this folder before building:
|
||||
|
||||
Filename required:
|
||||
- thefeed-client
|
||||
|
||||
How to produce it from project root:
|
||||
- make build-android-arm64
|
||||
- cp build/thefeed-client-android-arm64 android/app/src/main/assets/thefeed-client
|
||||
|
||||
The app copies this file to internal storage, marks it executable, and runs it as:
|
||||
- --data-dir <app files dir>/thefeeddata
|
||||
- --port 8080
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.thefeed.android
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var txtStatus: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
webView = findViewById(R.id.webView)
|
||||
txtStatus = findViewById(R.id.txtStatus)
|
||||
|
||||
findViewById<Button>(R.id.btnStart).setOnClickListener {
|
||||
startThefeedService()
|
||||
txtStatus.text = "Service started. Opening local UI..."
|
||||
loadLocalWeb()
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.btnStop).setOnClickListener {
|
||||
stopService(Intent(this, ThefeedService::class.java))
|
||||
txtStatus.text = "Service stopped"
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.btnReload).setOnClickListener {
|
||||
loadLocalWeb()
|
||||
}
|
||||
|
||||
configureWebView()
|
||||
|
||||
// Start in background by default so WebView can connect on first launch.
|
||||
startThefeedService()
|
||||
loadLocalWeb()
|
||||
}
|
||||
|
||||
private fun startThefeedService() {
|
||||
val intent = Intent(this, ThefeedService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureWebView() {
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
txtStatus.text = "Connected to local UI"
|
||||
}
|
||||
}
|
||||
|
||||
with(webView.settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
allowFileAccess = false
|
||||
allowContentAccess = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLocalWeb() {
|
||||
txtStatus.text = "Loading http://127.0.0.1:8080 ..."
|
||||
webView.postDelayed({ webView.loadUrl("http://127.0.0.1:8080") }, 500)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
webView.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.thefeed.android
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class ThefeedService : Service() {
|
||||
private var process: Process? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
startForeground(NOTIFICATION_ID, buildNotification("Starting local service..."))
|
||||
startClientProcess()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
process?.destroy()
|
||||
process = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
private fun startClientProcess() {
|
||||
try {
|
||||
val bin = ensureBinary()
|
||||
val dataDir = File(filesDir, "thefeeddata")
|
||||
if (!dataDir.exists()) {
|
||||
dataDir.mkdirs()
|
||||
}
|
||||
|
||||
val pb = ProcessBuilder(
|
||||
bin.absolutePath,
|
||||
"--data-dir", dataDir.absolutePath,
|
||||
"--port", "8080"
|
||||
)
|
||||
pb.redirectErrorStream(true)
|
||||
process = pb.start()
|
||||
|
||||
val outputReader = process?.inputStream?.bufferedReader()
|
||||
Thread {
|
||||
try {
|
||||
while (true) {
|
||||
val line = outputReader?.readLine() ?: break
|
||||
updateForegroundNotification(line)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}.start()
|
||||
|
||||
updateForegroundNotification("Running on http://127.0.0.1:8080")
|
||||
} catch (e: Exception) {
|
||||
updateForegroundNotification("Failed: ${e.message}")
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureBinary(): File {
|
||||
val target = File(filesDir, "thefeed-client")
|
||||
if (target.exists() && target.length() > 0L && target.canExecute()) {
|
||||
return target
|
||||
}
|
||||
|
||||
assets.open("thefeed-client").use { input ->
|
||||
FileOutputStream(target).use { out ->
|
||||
input.copyTo(out)
|
||||
}
|
||||
}
|
||||
|
||||
if (!target.setExecutable(true, true)) {
|
||||
throw IllegalStateException("Could not set executable bit on bundled binary")
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"thefeed background service",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(message: String): Notification {
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("thefeed")
|
||||
.setContentText(message)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun updateForegroundNotification(message: String) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.notify(NOTIFICATION_ID, buildNotification(message))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "thefeed_service"
|
||||
private const val NOTIFICATION_ID = 1201
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/bg">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/bgPanel">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnStart"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/start_service" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnStop"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/stop_service" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnReload"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/reload_webview" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/webview_loading" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<color name="bg">#0B1020</color>
|
||||
<color name="bgPanel">#132247</color>
|
||||
<color name="accent">#F59E0B</color>
|
||||
<color name="text">#EDF2FF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<string name="app_name">thefeed</string>
|
||||
<string name="webview_loading">Loading local web UI...</string>
|
||||
<string name="start_service">Start</string>
|
||||
<string name="stop_service">Stop</string>
|
||||
<string name="reload_webview">Reload</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.Thefeed" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/bgPanel</item>
|
||||
<item name="colorPrimaryVariant">@color/bgPanel</item>
|
||||
<item name="colorOnPrimary">@color/text</item>
|
||||
<item name="android:statusBarColor" tools:targetApi="l">@color/bg</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.7.3' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '2.0.21' apply false
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "thefeed-android"
|
||||
include ':app'
|
||||
Reference in New Issue
Block a user