Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,107 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>

<!-- Weather Widget (標準版 4x3) -->
<receiver
android:name=".WeatherWidgetProvider"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/weather_widget_info"/>
</receiver>

<!-- Weather Widget Small (小方形版 2x2) -->
<receiver
android:name=".WeatherWidgetSmallProvider"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/weather_widget_small_info"/>
</receiver>

<!-- Workmanager 背景任務服務 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>

<!-- WorkManager Worker -->
<service
android:name="androidx.work.impl.background.systemjob.SystemJobService"
android:directBootAware="false"
android:enabled="@bool/enable_system_job_service_default"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"
tools:targetApi="n" />

<service
android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
android:directBootAware="false"
android:enabled="@bool/enable_system_alarm_service_default"
android:exported="false" />

<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:directBootAware="false"
android:enabled="@bool/enable_system_foreground_service_default"
android:exported="false" />

<receiver
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryChargingProxy"
android:directBootAware="false"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
</intent-filter>
</receiver>

<receiver
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
android:directBootAware="false"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.TIME_SET" />
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
</intent-filter>
</receiver>

<receiver
android:name="androidx.work.impl.background.systemalarm.ConstraintProxyUpdateReceiver"
android:directBootAware="false"
android:enabled="@bool/enable_system_alarm_service_default"
android:exported="false">
<intent-filter>
<action android:name="androidx.work.impl.background.systemalarm.UpdateProxies" />
</intent-filter>
</receiver>

<receiver
android:name="androidx.work.impl.diagnostics.DiagnosticsReceiver"
android:directBootAware="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.DUMP">
<intent-filter>
<action android:name="androidx.work.diagnostics.REQUEST_DIAGNOSTICS" />
</intent-filter>
</receiver>
</application>
<queries>
<intent>
Expand Down
59 changes: 58 additions & 1 deletion android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
package com.exptech.dpip

import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private val WIDGET_CHANNEL = "com.exptech.dpip/widget"

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, WIDGET_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"updateWidgets" -> {
try {
updateAllWidgets()
result.success(true)
} catch (e: Exception) {
result.error("UPDATE_ERROR", "Failed to update widgets: ${e.message}", null)
}
}
else -> {
result.notImplemented()
}
}
}
}

/**
* 手動觸發所有 widget 實例的更新
* 發送 APPWIDGET_UPDATE broadcast 來觸發 onUpdate 方法
*/
private fun updateAllWidgets() {
val context = applicationContext

// 更新標準版 widget
val standardManager = AppWidgetManager.getInstance(context)
val standardComponent = ComponentName(context, WeatherWidgetProvider::class.java)
val standardIds = standardManager.getAppWidgetIds(standardComponent)
if (standardIds.isNotEmpty()) {
val intent = Intent(context, WeatherWidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, standardIds)
context.sendBroadcast(intent)
}

// 更新小方形版 widget
val smallComponent = ComponentName(context, WeatherWidgetSmallProvider::class.java)
val smallIds = standardManager.getAppWidgetIds(smallComponent)
if (smallIds.isNotEmpty()) {
val intent = Intent(context, WeatherWidgetSmallProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, smallIds)
context.sendBroadcast(intent)
}
}
}
138 changes: 138 additions & 0 deletions android/app/src/main/kotlin/com/exptech/dpip/WeatherWidgetProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package com.exptech.dpip

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
import es.antonborri.home_widget.HomeWidgetPlugin

/**
* DPIP 天氣桌面小部件
* 顯示即時天氣資訊
*/
class WeatherWidgetProvider : AppWidgetProvider() {

override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// 更新所有小部件實例
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}

override fun onEnabled(context: Context) {
// 第一次添加小部件時呼叫
}

override fun onDisabled(context: Context) {
// 最後一個小部件被移除時呼叫
}

companion object {
/**
* 更新單個小部件
*/
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
// 從 SharedPreferences 讀取資料
val widgetData = HomeWidgetPlugin.getData(context)
val views = RemoteViews(context.packageName, R.layout.weather_widget)

// 檢查是否有錯誤或沒有資料
val hasError = widgetData.getBoolean("has_error", false)
val hasData = widgetData.contains("temperature")

if (hasError || !hasData) {
val errorMessage = widgetData.getString("error_message", "無法載入天氣")
views.setTextViewText(R.id.weather_status, errorMessage)
views.setTextViewText(R.id.temperature, "--°")
} else {
// 天氣狀態
val weatherStatus = widgetData.getString("weather_status", "晴天")
views.setTextViewText(R.id.weather_status, weatherStatus)

// 溫度
val temperature = widgetData.readIntValue("temperature") ?: 0
views.setTextViewText(R.id.temperature, "${temperature}°")

// 體感溫度
val feelsLike = widgetData.readIntValue("feels_like") ?: 0
views.setTextViewText(R.id.feels_like, "體感 ${feelsLike}°")

// 濕度
val humidity = widgetData.readIntValue("humidity") ?: 0
views.setTextViewText(R.id.humidity, "${humidity}%")

// 風速
val windSpeed = widgetData.readNumber("wind_speed") ?: 0.0
views.setTextViewText(R.id.wind_speed, String.format("%.1fm/s", windSpeed))

// 風向
val windDirection = widgetData.getString("wind_direction", "-")
views.setTextViewText(R.id.wind_direction, windDirection)

// 降雨
val rain = widgetData.readNumber("rain") ?: 0.0
views.setTextViewText(R.id.rain, String.format("%.1fmm", rain))

// 氣象站
val stationName = widgetData.getString("station_name", "")
val stationDistance = widgetData.readNumber("station_distance") ?: 0.0
views.setTextViewText(
R.id.station_info,
"${stationName}氣象站 · ${String.format("%.1f", stationDistance)}km"
)

// 更新時間
val updateTime = widgetData.readTimestampMillis("update_time")
if (updateTime != null && updateTime > 0) {
val calendar = java.util.Calendar.getInstance()
calendar.timeInMillis = updateTime
val timeStr = String.format(
"%02d:%02d",
calendar.get(java.util.Calendar.HOUR_OF_DAY),
calendar.get(java.util.Calendar.MINUTE)
)
views.setTextViewText(R.id.update_time, timeStr)
}

// 天氣圖示 (根據 weatherCode 設定)
val weatherCode = widgetData.getInt("weather_code", 1)
val iconRes = getWeatherIcon(weatherCode)
views.setImageViewResource(R.id.weather_icon, iconRes)
}

// 點擊小部件開啟 App
val pendingIntent = es.antonborri.home_widget.HomeWidgetLaunchIntent.getActivity(
context,
MainActivity::class.java
)
views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)

// 更新小部件
appWidgetManager.updateAppWidget(appWidgetId, views)
}

/**
* 根據天氣代碼返回對應圖示
* 對應到 Flutter 的 WeatherIcons.getWeatherIcon
*/
fun getWeatherIcon(code: Int): Int {
return when (code) {
1 -> android.R.drawable.ic_menu_day // 晴天
2, 3 -> android.R.drawable.ic_partial_secure // 多雲
4, 5, 6, 7 -> android.R.drawable.ic_dialog_alert // 陰天/霧
8, 9, 10, 11, 12, 13, 14 -> android.R.drawable.ic_dialog_info // 雨天
15, 16, 17, 18 -> android.R.drawable.ic_lock_power_off // 雷雨
else -> android.R.drawable.ic_menu_day
}
// 注意: 實際使用時應該使用自訂圖示,這裡使用系統圖示作為範例
}
}
}
Loading
Loading