diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c1d36c21..ae394adf 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -46,6 +46,15 @@
android:name="flutterEmbedding"
android:value="2" />
+
+
+
+
+
+
()
+ }
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
@@ -27,105 +38,184 @@ class HomeWidget : AppWidgetProvider() {
}
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
- val views = RemoteViews(context.packageName, R.layout.home_widget)
- val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
- var url = sp.getString("widget_$appWidgetId", null)
- if (url.isNullOrEmpty()) {
- url = sp.getString("$appWidgetId", null)
- }
- if (url.isNullOrEmpty()) {
- val gUrl = sp.getString("widget_*", null)
- url = gUrl
- }
-
- if (url.isNullOrEmpty()) {
- Log.e("HomeWidget", "URL not found")
- }
-
- val intentUpdate = Intent(context, HomeWidget::class.java)
- intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
- val ids = intArrayOf(appWidgetId)
- intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
-
- var flag = PendingIntent.FLAG_UPDATE_CURRENT
- if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
- flag = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- }
-
- val pendingUpdate: PendingIntent = PendingIntent.getBroadcast(
- context,
- appWidgetId,
- intentUpdate,
- flag)
- views.setOnClickPendingIntent(R.id.widget_container, pendingUpdate)
-
- if (url.isNullOrEmpty()) {
- views.setTextViewText(R.id.widget_name, "No URL")
- // Update the widget to display a message for missing URL
- views.setViewVisibility(R.id.error_message, View.VISIBLE)
- views.setTextViewText(R.id.error_message, "Please configure the widget URL.")
- views.setViewVisibility(R.id.widget_content, View.GONE)
- views.setFloat(R.id.widget_name, "setAlpha", 1f)
- views.setFloat(R.id.error_message, "setAlpha", 1f)
- appWidgetManager.updateAppWidget(appWidgetId, views)
+ // Prevent concurrent updates for the same widget
+ if (activeUpdates.putIfAbsent(appWidgetId, true) == true) {
+ Log.d(TAG, "Widget $appWidgetId is already updating, skipping")
return
- } else {
- views.setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
- views.setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
- views.setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
- views.setViewVisibility(R.id.widget_net_label, View.VISIBLE)
}
+ val views = RemoteViews(context.packageName, R.layout.home_widget)
+ val url = getWidgetUrl(context, appWidgetId)
+
+ if (url.isNullOrEmpty()) {
+ Log.w(TAG, "URL not found for widget $appWidgetId")
+ showErrorState(views, appWidgetManager, appWidgetId, "Please configure the widget URL.")
+ activeUpdates.remove(appWidgetId)
+ return
+ }
+
+ setupClickIntent(context, views, appWidgetId)
+
+ showLoadingState(views, appWidgetManager, appWidgetId)
+
CoroutineScope(Dispatchers.IO).launch {
- try {
- val connection = URL(url).openConnection() as HttpURLConnection
- connection.requestMethod = "GET"
- val responseCode = connection.responseCode
- if (responseCode == HttpURLConnection.HTTP_OK) {
- val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
- val jsonObject = JSONObject(jsonStr)
- val data = jsonObject.getJSONObject("data")
- val server = data.getString("name")
- val cpu = data.getString("cpu")
- val mem = data.getString("mem")
- val disk = data.getString("disk")
- val net = data.getString("net")
- withContext(Dispatchers.Main) {
- if (mem.isEmpty() || disk.isEmpty()) {
- Log.e("HomeWidget", "Failed to retrieve status: Memory or disk information is empty")
- return@withContext
+ withTimeoutOrNull(COROUTINE_TIMEOUT) {
+ try {
+ val serverData = fetchServerData(url)
+ if (serverData != null) {
+ withContext(Dispatchers.Main) {
+ showSuccessState(views, appWidgetManager, appWidgetId, serverData)
+ }
+ } else {
+ withContext(Dispatchers.Main) {
+ showErrorState(views, appWidgetManager, appWidgetId, "Invalid server data received.")
}
- views.setTextViewText(R.id.widget_name, server)
- views.setTextViewText(R.id.widget_cpu, cpu)
- views.setTextViewText(R.id.widget_mem, mem)
- views.setTextViewText(R.id.widget_disk, disk)
- views.setTextViewText(R.id.widget_net, net)
- val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
- views.setTextViewText(R.id.widget_time, timeStr)
- views.setFloat(R.id.widget_name, "setAlpha", 1f)
- views.setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
- views.setFloat(R.id.widget_mem_label, "setAlpha", 1f)
- views.setFloat(R.id.widget_disk_label, "setAlpha", 1f)
- views.setFloat(R.id.widget_net_label, "setAlpha", 1f)
- views.setFloat(R.id.widget_time, "setAlpha", 1f)
- appWidgetManager.updateAppWidget(appWidgetId, views)
}
- } else {
- throw FileNotFoundException("HTTP response code: $responseCode")
+ } catch (e: Exception) {
+ Log.e(TAG, "Error updating widget $appWidgetId: ${e.message}", e)
+ withContext(Dispatchers.Main) {
+ val errorMessage = when (e) {
+ is SocketTimeoutException -> "Connection timeout. Please check your network."
+ is IOException -> "Network error. Please check your connection."
+ is JSONException -> "Invalid data format received from server."
+ else -> "Failed to retrieve data: ${e.message}"
+ }
+ showErrorState(views, appWidgetManager, appWidgetId, errorMessage)
+ }
}
- } catch (e: Exception) {
- Log.e("HomeWidget", "Error updating widget: ${e.localizedMessage}", e)
+ } ?: run {
+ Log.w(TAG, "Widget update timed out for widget $appWidgetId")
withContext(Dispatchers.Main) {
- views.setTextViewText(R.id.widget_name, "Error")
- // Update the widget to display a message for data retrieval failure
- views.setViewVisibility(R.id.error_message, View.VISIBLE)
- views.setTextViewText(R.id.error_message, "Failed to retrieve data.")
- views.setViewVisibility(R.id.widget_content, View.GONE)
- views.setFloat(R.id.widget_name, "setAlpha", 1f)
- views.setFloat(R.id.error_message, "setAlpha", 1f)
- appWidgetManager.updateAppWidget(appWidgetId, views)
+ showErrorState(views, appWidgetManager, appWidgetId, "Update timed out. Please try again.")
}
}
+ activeUpdates.remove(appWidgetId)
}
}
+
+ private fun getWidgetUrl(context: Context, appWidgetId: Int): String? {
+ val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
+ return sp.getString("widget_$appWidgetId", null)
+ ?: sp.getString("$appWidgetId", null)
+ ?: sp.getString("widget_*", null)
+ }
+
+ private fun setupClickIntent(context: Context, views: RemoteViews, appWidgetId: Int) {
+ val intentConfigure = Intent(context, WidgetConfigureActivity::class.java).apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ }
+
+ val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+
+ val pendingConfigure = PendingIntent.getActivity(context, appWidgetId, intentConfigure, flag)
+ views.setOnClickPendingIntent(R.id.widget_container, pendingConfigure)
+ }
+
+ private suspend fun fetchServerData(url: String): ServerData? = withContext(Dispatchers.IO) {
+ var connection: HttpURLConnection? = null
+ try {
+ connection = (URL(url).openConnection() as HttpURLConnection).apply {
+ requestMethod = "GET"
+ connectTimeout = NETWORK_TIMEOUT.toInt()
+ readTimeout = NETWORK_TIMEOUT.toInt()
+ setRequestProperty("User-Agent", "ServerBox-Widget/1.0")
+ setRequestProperty("Accept", "application/json")
+ }
+
+ if (connection.responseCode != HttpURLConnection.HTTP_OK) {
+ throw IOException("HTTP ${connection.responseCode}: ${connection.responseMessage}")
+ }
+
+ val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
+ parseServerData(jsonStr)
+ } finally {
+ connection?.disconnect()
+ }
+ }
+
+ private fun parseServerData(jsonStr: String): ServerData? {
+ return try {
+ val jsonObject = JSONObject(jsonStr)
+ val data = jsonObject.getJSONObject("data")
+
+ val server = data.optString("name", "Unknown Server")
+ val cpu = data.optString("cpu", "").takeIf { it.isNotBlank() } ?: "N/A"
+ val mem = data.optString("mem", "").takeIf { it.isNotBlank() } ?: "N/A"
+ val disk = data.optString("disk", "").takeIf { it.isNotBlank() } ?: "N/A"
+ val net = data.optString("net", "").takeIf { it.isNotBlank() } ?: "N/A"
+
+ // Return data even if some fields are missing, providing defaults
+ // Only reject if we can't parse the JSON structure properly
+ ServerData(server, cpu, mem, disk, net)
+ } catch (e: JSONException) {
+ Log.e(TAG, "JSON parsing error: ${e.message}", e)
+ null
+ }
+ }
+
+ private fun showLoadingState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
+ views.apply {
+ setTextViewText(R.id.widget_name, "Loading...")
+ setViewVisibility(R.id.error_message, View.GONE)
+ setViewVisibility(R.id.widget_content, View.VISIBLE)
+ setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
+ setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
+ setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
+ setViewVisibility(R.id.widget_net_label, View.VISIBLE)
+ setViewVisibility(R.id.widget_progress, View.VISIBLE)
+ setFloat(R.id.widget_name, "setAlpha", 0.7f)
+ }
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
+
+ private fun showSuccessState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, data: ServerData) {
+ views.apply {
+ setTextViewText(R.id.widget_name, data.name)
+ setTextViewText(R.id.widget_cpu, data.cpu)
+ setTextViewText(R.id.widget_mem, data.mem)
+ setTextViewText(R.id.widget_disk, data.disk)
+ setTextViewText(R.id.widget_net, data.net)
+
+ val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
+ setTextViewText(R.id.widget_time, timeStr)
+
+ setViewVisibility(R.id.error_message, View.GONE)
+ setViewVisibility(R.id.widget_content, View.VISIBLE)
+ setViewVisibility(R.id.widget_progress, View.GONE)
+
+ // Smooth fade-in animation
+ setFloat(R.id.widget_name, "setAlpha", 1f)
+ setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
+ setFloat(R.id.widget_mem_label, "setAlpha", 1f)
+ setFloat(R.id.widget_disk_label, "setAlpha", 1f)
+ setFloat(R.id.widget_net_label, "setAlpha", 1f)
+ setFloat(R.id.widget_time, "setAlpha", 1f)
+ }
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
+
+ private fun showErrorState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, errorMessage: String) {
+ views.apply {
+ setTextViewText(R.id.widget_name, "Error")
+ setViewVisibility(R.id.error_message, View.VISIBLE)
+ setTextViewText(R.id.error_message, errorMessage)
+ setViewVisibility(R.id.widget_content, View.GONE)
+ setViewVisibility(R.id.widget_progress, View.GONE)
+ setFloat(R.id.widget_name, "setAlpha", 1f)
+ setFloat(R.id.error_message, "setAlpha", 1f)
+ }
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
+
+ data class ServerData(
+ val name: String,
+ val cpu: String,
+ val mem: String,
+ val disk: String,
+ val net: String
+ )
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/tech/lolli/toolbox/widget/WidgetConfigureActivity.kt b/android/app/src/main/kotlin/tech/lolli/toolbox/widget/WidgetConfigureActivity.kt
new file mode 100644
index 00000000..f87e0431
--- /dev/null
+++ b/android/app/src/main/kotlin/tech/lolli/toolbox/widget/WidgetConfigureActivity.kt
@@ -0,0 +1,82 @@
+package tech.lolli.toolbox.widget
+
+import android.app.Activity
+import android.appwidget.AppWidgetManager
+import android.content.Intent
+import android.os.Bundle
+import android.util.Patterns
+import android.widget.Button
+import android.widget.EditText
+import tech.lolli.toolbox.R
+
+class WidgetConfigureActivity : Activity() {
+ private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
+ private lateinit var urlEditText: EditText
+ private lateinit var saveButton: Button
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.widget_configure)
+
+ // 设置结果为取消,以防用户在完成配置前退出
+ setResult(RESULT_CANCELED)
+
+ // 获取 widget ID
+ val extras = intent.extras
+ if (extras != null) {
+ appWidgetId = extras.getInt(
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID
+ )
+ }
+
+ // 如果没有有效的 widget ID,完成 activity
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ finish()
+ return
+ }
+
+ // 初始化 UI 元素
+ urlEditText = findViewById(R.id.url_edit_text)
+ saveButton = findViewById(R.id.save_button)
+
+ // 从 SharedPreferences 加载现有配置
+ val sp = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE)
+ val existingUrl = sp.getString("widget_$appWidgetId", "")
+ urlEditText.setText(existingUrl)
+
+ // 设置保存按钮点击事件
+ saveButton.setOnClickListener {
+ val url = urlEditText.text.toString().trim()
+ if (url.isEmpty()) {
+ urlEditText.error = "Please enter a URL"
+ return@setOnClickListener
+ }
+
+ // 验证 URL 格式
+ if (!Patterns.WEB_URL.matcher(url).matches()) {
+ urlEditText.error = "Please enter a valid URL"
+ return@setOnClickListener
+ }
+
+ // 保存 URL 到 SharedPreferences
+ val editor = sp.edit()
+ editor.putString("widget_$appWidgetId", url)
+ editor.apply()
+
+ // 更新 widget 使用 AppWidgetManager
+ val appWidgetManager = AppWidgetManager.getInstance(this)
+ val updateIntent = Intent(this, HomeWidget::class.java).apply {
+ action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
+ }
+ sendBroadcast(updateIntent)
+
+ // 设置结果并结束 activity
+ val resultValue = Intent()
+ resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ setResult(RESULT_OK, resultValue)
+ finish()
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/home_widget.xml b/android/app/src/main/res/layout/home_widget.xml
index af31dbde..89f45b85 100644
--- a/android/app/src/main/res/layout/home_widget.xml
+++ b/android/app/src/main/res/layout/home_widget.xml
@@ -10,14 +10,17 @@
@@ -27,121 +30,138 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@id/widget_name"
- android:paddingTop="13dp">
+ android:layout_marginTop="8dp">
+ android:orientation="horizontal"
+ android:alpha="0"
+ android:animateLayoutChanges="true">
-
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:src="@drawable/speed_24"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="CPU usage" />
+ android:textSize="12sp"
+ tools:text="CPU: 25.6%" />
+ android:orientation="horizontal"
+ android:alpha="0"
+ android:animateLayoutChanges="true">
-
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:src="@drawable/memory_24"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="Memory usage" />
+ android:textSize="12sp"
+ tools:text="Memory: 4.2GB / 8GB" />
+ android:orientation="horizontal"
+ android:alpha="0"
+ android:animateLayoutChanges="true">
-
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:src="@drawable/storage_24"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="Disk usage" />
+ android:textSize="12sp"
+ tools:text="Disk: 125GB / 250GB" />
+ android:orientation="horizontal"
+ android:alpha="0"
+ android:animateLayoutChanges="true">
-
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:src="@drawable/net_24"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="Network usage" />
+ android:textSize="12sp"
+ tools:text="Network: 15MB/s ↓ 8MB/s ↑" />
@@ -149,29 +169,45 @@
-
+
+ android:lineSpacingMultiplier="1.2"
+ android:maxLines="3"
+ android:ellipsize="end"
+ tools:text="Error message text that might be longer than usual" />
+ android:fontFamily="monospace"
+ tools:text="12:34" />
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/widget_configure.xml b/android/app/src/main/res/layout/widget_configure.xml
new file mode 100644
index 00000000..879e462b
--- /dev/null
+++ b/android/app/src/main/res/layout/widget_configure.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/home_widget.xml b/android/app/src/main/res/xml/home_widget.xml
index b26ca598..1c0ddc37 100644
--- a/android/app/src/main/res/xml/home_widget.xml
+++ b/android/app/src/main/res/xml/home_widget.xml
@@ -6,6 +6,7 @@
android:minHeight="110dp"
android:updatePeriodMillis="1800001"
android:initialLayout="@layout/home_widget"
+ android:configure="tech.lolli.toolbox.widget.WidgetConfigureActivity"
android:resizeMode="none"
android:widgetCategory="home_screen">
\ No newline at end of file