Rebrand CommGate to gsmnode and adopt new design system
Rename the whole project from CommGate to gsmnode and re-skin every surface onto the new signal-green + ink design system (Space Grotesk / IBM Plex Sans / JetBrains Mono, flat surfaces, two-arrow routing mark, lowercase gsm+node wordmark). - Web App + API panel: rewrite token layer, gn-* utilities, data-gsm-theme, new logo/favicon assets; both builds verified. - Phone App (Flutter): green theme, new mark/wordmark widgets, adaptive launcher icons; rename Android applicationId/package to app.gsmnode.phone; bump AGP to 8.6.0 and google_fonts to 8.1.0; debug APK build verified. - API Server (Go): panel routes, User-Agent, health service id, branding. - Home Assistant Plugin: rename integration domain sms_gateway to gsmnode (folder, DOMAIN, services, entity ids, classes); py_compile verified. - Update all READMEs; add .gitignore (excludes Design/, build artifacts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -0,0 +1,43 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.gsmnode.phone"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.gsmnode.phone"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,76 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Sending and receiving SMS -->
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_SMS" />
|
||||
<!-- Placing outbound calls remotely -->
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<!-- Optional: needed to choose a specific SIM on dual-SIM devices -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- Keep the gateway alive in the background (foreground service path) -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<!-- Android 13+ runtime permission for the ongoing service notification -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:label="gsmnode"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Incoming SMS broadcast receiver -->
|
||||
<receiver
|
||||
android:name=".SmsReceiver"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BROADCAST_SMS">
|
||||
<intent-filter android:priority="999">
|
||||
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Receives send/delivery outcomes from PendingIntents (explicit) -->
|
||||
<receiver
|
||||
android:name=".SmsStatusReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Foreground service that keeps the gateway loop alive -->
|
||||
<service
|
||||
android:name=".GatewayForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<!-- Required so plugins (e.g. url launches) can query intents on Android 11+ -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,99 @@
|
||||
package app.gsmnode.phone
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/// Foreground service that keeps the app process alive (and the CPU awake via a
|
||||
/// partial wakelock) so the Dart gateway loop keeps polling/sending while the
|
||||
/// screen is off or the app is in the background.
|
||||
///
|
||||
/// Note: this closes the "survives screen lock" gap. Surviving a full task
|
||||
/// removal or Doze for hours would additionally need a background Dart isolate
|
||||
/// (e.g. flutter_background_service) — documented as a further step.
|
||||
class GatewayForegroundService : Service() {
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "sms_gateway_service"
|
||||
private const val NOTIFICATION_ID = 4711
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, GatewayForegroundService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, GatewayForegroundService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startForegroundCompat()
|
||||
acquireWakeLock()
|
||||
// Restart if the system kills the service.
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun startForegroundCompat() {
|
||||
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("gsmnode")
|
||||
.setContentText("Gateway is running")
|
||||
.setSmallIcon(android.R.drawable.stat_sys_upload)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID, notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("WakelockTimeout")
|
||||
private fun acquireWakeLock() {
|
||||
if (wakeLock?.isHeld == true) return
|
||||
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
wakeLock = pm.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "gsmnode:gateway"
|
||||
).also { it.acquire() }
|
||||
}
|
||||
|
||||
private fun createChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID, "gsmnode",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply { description = "Keeps the SMS gateway running" }
|
||||
getSystemService(NotificationManager::class.java)
|
||||
.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
wakeLock?.let { if (it.isHeld) it.release() }
|
||||
wakeLock = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package app.gsmnode.phone
|
||||
|
||||
import android.Manifest
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.telecom.TelecomManager
|
||||
import android.telephony.SmsManager
|
||||
import android.telephony.SubscriptionManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
companion object {
|
||||
const val METHOD_CHANNEL = "app.gsmnode/sms"
|
||||
const val EVENT_CHANNEL = "app.gsmnode/sms_incoming"
|
||||
const val STATUS_CHANNEL = "app.gsmnode/sms_status"
|
||||
const val STATUS_ACTION = "app.gsmnode.SMS_STATUS"
|
||||
|
||||
// Sinks used by the broadcast receivers to push events into Dart.
|
||||
@Volatile var incomingSink: EventChannel.EventSink? = null
|
||||
@Volatile var statusSink: EventChannel.EventSink? = null
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
|
||||
MethodChannel(messenger, METHOD_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"sendSms" -> {
|
||||
val phone = call.argument<String>("phone")
|
||||
val message = call.argument<String>("message")
|
||||
val simSlot = call.argument<Int>("simSlot")
|
||||
val messageId = call.argument<String>("messageId")
|
||||
if (phone.isNullOrBlank() || message == null) {
|
||||
result.error("BAD_ARGS", "phone and message are required", null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
try {
|
||||
sendSms(phone, message, simSlot, messageId)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("SEND_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
"placeCall" -> {
|
||||
val phone = call.argument<String>("phone")
|
||||
if (phone.isNullOrBlank()) {
|
||||
result.error("BAD_ARGS", "phone is required", null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
try {
|
||||
placeCall(phone)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("CALL_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
"startService" -> {
|
||||
GatewayForegroundService.start(this)
|
||||
result.success(true)
|
||||
}
|
||||
"stopService" -> {
|
||||
GatewayForegroundService.stop(this)
|
||||
result.success(true)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
EventChannel(messenger, EVENT_CHANNEL).setStreamHandler(
|
||||
object : EventChannel.StreamHandler {
|
||||
override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
|
||||
incomingSink = sink
|
||||
}
|
||||
override fun onCancel(args: Any?) {
|
||||
incomingSink = null
|
||||
}
|
||||
})
|
||||
|
||||
EventChannel(messenger, STATUS_CHANNEL).setStreamHandler(
|
||||
object : EventChannel.StreamHandler {
|
||||
override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
|
||||
statusSink = sink
|
||||
}
|
||||
override fun onCancel(args: Any?) {
|
||||
statusSink = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun smsManagerFor(simSlot: Int?): SmsManager {
|
||||
if (simSlot == null) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
getSystemService(SmsManager::class.java)
|
||||
else
|
||||
@Suppress("DEPRECATION") SmsManager.getDefault()
|
||||
}
|
||||
val hasPerm = ContextCompat.checkSelfPermission(
|
||||
this, Manifest.permission.READ_PHONE_STATE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
if (hasPerm) {
|
||||
val sm = getSystemService(SubscriptionManager::class.java)
|
||||
val info = sm?.getActiveSubscriptionInfoForSimSlotIndex(simSlot)
|
||||
if (info != null) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
getSystemService(SmsManager::class.java)
|
||||
.createForSubscriptionId(info.subscriptionId)
|
||||
else
|
||||
@Suppress("DEPRECATION")
|
||||
SmsManager.getSmsManagerForSubscriptionId(info.subscriptionId)
|
||||
}
|
||||
}
|
||||
return smsManagerFor(null)
|
||||
}
|
||||
|
||||
/// Builds a PendingIntent that, when fired by the radio, broadcasts the
|
||||
/// send/delivery outcome to SmsStatusReceiver (tagged with the message id).
|
||||
private fun statusPendingIntent(messageId: String?, phone: String, kind: String): PendingIntent {
|
||||
val intent = Intent(this, SmsStatusReceiver::class.java).apply {
|
||||
action = STATUS_ACTION
|
||||
putExtra("messageId", messageId)
|
||||
putExtra("phone", phone)
|
||||
putExtra("kind", kind)
|
||||
}
|
||||
val requestCode = (messageId.orEmpty() + phone + kind).hashCode()
|
||||
return PendingIntent.getBroadcast(
|
||||
this, requestCode, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
/// Places a phone call. Requires the CALL_PHONE permission (requested in Dart
|
||||
/// via the phone permission group).
|
||||
///
|
||||
/// Uses TelecomManager.placeCall, which routes through the system telecom
|
||||
/// service rather than starting the dialer activity ourselves. This is what
|
||||
/// lets the call go through when the screen is locked / the app is in the
|
||||
/// background — a plain startActivity(ACTION_CALL) is blocked by Android's
|
||||
/// background-activity-start restrictions in that state. Falls back to
|
||||
/// ACTION_CALL only on very old devices without TelecomManager.placeCall.
|
||||
private fun placeCall(phone: String) {
|
||||
val uri = Uri.fromParts("tel", phone, null)
|
||||
val hasPerm = ContextCompat.checkSelfPermission(
|
||||
this, Manifest.permission.CALL_PHONE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
if (!hasPerm) {
|
||||
throw SecurityException("CALL_PHONE permission not granted")
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val telecom = getSystemService(TelecomManager::class.java)
|
||||
if (telecom != null) {
|
||||
telecom.placeCall(uri, Bundle())
|
||||
return
|
||||
}
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_CALL, uri)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun sendSms(phone: String, message: String, simSlot: Int?, messageId: String?) {
|
||||
val sms = smsManagerFor(simSlot)
|
||||
val parts = sms.divideMessage(message)
|
||||
val sentPI = statusPendingIntent(messageId, phone, "sent")
|
||||
val deliveredPI = statusPendingIntent(messageId, phone, "delivered")
|
||||
if (parts.size > 1) {
|
||||
val sentList = ArrayList<PendingIntent>(parts.size)
|
||||
val deliveredList = ArrayList<PendingIntent>(parts.size)
|
||||
for (i in parts.indices) {
|
||||
sentList.add(sentPI)
|
||||
deliveredList.add(deliveredPI)
|
||||
}
|
||||
sms.sendMultipartTextMessage(phone, null, parts, sentList, deliveredList)
|
||||
} else {
|
||||
sms.sendTextMessage(phone, null, message, sentPI, deliveredPI)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package app.gsmnode.phone
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Telephony
|
||||
|
||||
/// Receives incoming SMS and forwards each message to Dart via the EventChannel
|
||||
/// sink held by MainActivity. Works while the app process is alive.
|
||||
class SmsReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return
|
||||
|
||||
val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) ?: return
|
||||
if (messages.isEmpty()) return
|
||||
|
||||
// Multipart SMS arrive as several PDUs from the same sender; concatenate.
|
||||
val from = messages[0].displayOriginatingAddress ?: ""
|
||||
val body = StringBuilder()
|
||||
var timestamp = System.currentTimeMillis()
|
||||
for (m in messages) {
|
||||
body.append(m.displayMessageBody ?: "")
|
||||
timestamp = m.timestampMillis
|
||||
}
|
||||
|
||||
val payload = mapOf(
|
||||
"from" to from,
|
||||
"body" to body.toString(),
|
||||
"timestamp" to timestamp,
|
||||
)
|
||||
|
||||
// EventSink must be touched on the main thread.
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.incomingSink?.success(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.gsmnode.phone
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
/// Receives SMS send/delivery outcomes from the PendingIntents created in
|
||||
/// MainActivity.sendSms and forwards them to Dart, tagged with the message id
|
||||
/// so the gateway loop can report Sent / Delivered / Failed to the API Server.
|
||||
class SmsStatusReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val messageId = intent.getStringExtra("messageId")
|
||||
val kind = intent.getStringExtra("kind") ?: return
|
||||
// For "sent": RESULT_OK means the radio accepted the message.
|
||||
// For "delivered": RESULT_OK means a positive delivery report arrived.
|
||||
val success = resultCode == Activity.RESULT_OK
|
||||
|
||||
val payload = mapOf(
|
||||
"messageId" to messageId,
|
||||
"kind" to kind,
|
||||
"success" to success,
|
||||
"resultCode" to resultCode,
|
||||
)
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.statusSink?.success(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#2E9E6B</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||