https://github.com/kimmandoo/android-drill/tree/main/Widget
์์ ฏ์ ํ ๋ฒ ์์๋ณด์, ์ด๋ฒ์ ํ์ตํด๋ณด๋ฉด์ ๊ธฐ๋กํ ๋ ํฌ์งํ ๋ฆฌ๋ผ์ ๊ณ์ ๋ณ๊ฒฝ๋ ๊ฐ๋ฅ์ฑ์ด ์๋ค.
https://developer.android.com/develop/ui/views/appwidgets?hl=ko
์ผ๋จ ์ด๊ฑธ ํ ๋๋ก ์ ๋ฌธํด๋ดค๋ค.
์์ ฏ์ ํํ๋ฉด์์ ์ ๊ทผํ ์ ์๋ ์ฑ์ remote view๋ผ๊ณ ์๊ฐํด์ผ๋๋ค. ์ฑ๊ณผ๋ ๋ค๋ฅธ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ฐ์ง๋ฉฐ ๋ฐ๋ก๊ฐ๊ธฐ์ ํน์ฑ์ ๊ฐ์ง๊ณผ ๋์์ ์์ ฏ๋ง์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ฐ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ฑ์ ์๋ ์ค์ํ ๊ธฐ๋ฅ์ ๋ฐ๋ก ์์ ฏ์์ ์์ ํ ์ ์๋ค.
ํฌ๊ฒ 4๊ฐ์ง ์ข ๋ฅ๋ก ๊ตฌ๋ถํ๋ ๊ฒ ๊ฐ๋ค.
- ์ ๋ณด
- ์ปฌ๋ ์
- ๊ด๋ฆฌ
- ํ์ด๋ธ๋ฆฌ๋
์์ธํ ์ ๋ณด๋ ๊ณต์๋ฌธ์์ ๋ค์ด๊ฐ๋ฉด ๋์จ๋ค. ํ์ฌ ์์น์์ ์ด๋ค ๋ชฉ์ ์ง๊น์ง์ ๊ฑธ๋ฆฌ๋ ์๊ฐ๊ฐ์ ๊ฑธ ํ์ํ๋ ค๋ฉด ์ ๋ณด์์ ฏ์ด ์ ์ ํ๋ค๊ณ ์๊ฐํ๋ค. ๋ํ์ ์ผ๋ก ๋ ์จ ์์ ฏ์ด ์ ๋ณด ์์ ฏ์ ํด๋นํ๋ค. ๊ทผ๋ฐ ๊ณต๋ถ๋ฅผ ์ข ํด๋ณด๋ ์์น๋ฅผ ์ ๊ทน์ ์ผ๋ก ์ฌ์ฉํ๋ ค๋ฉด ์๋ ํ์ด์ง๋ฅผ ์ข ๋ ์ดํด๋ด์ผ๋ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค.
https://developer.android.com/about/versions/oreo/background-location-limits?hl=ko
# `AppWidgetProviderInfo`, `AppWidgetProvider`
์์ ฏ์ 2๊ฐ์ง ๊ตฌ์ฑ์์๊ฐ `ํ์`๋ค. AppWidgetProviderInfo๋ AppWidgetProvider๋ค. xml๋ก ์ ์๋ Info์ ๋ธ๋ก๋์บ์ค๋ ๋ฆฌ์๋ฒ๋ฅผ ์์๋ฐ๋ Provider๋ก ๋๋๊ณ , Info๋ Provider๋ฅผ ํฌํจํด์ ์์ ฏ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ค๋ช ํ๋ ๊ฐ์ฒด๋ค. xml๋ก ์์ฑํ๋ ๊ฐ์ฒด๋ผ์ ์ข ์ด์ํ๋ค.
`res/xml` ๋ฐ์ Info ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ค์ผ๋๋ค.
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/widget_layout"
android:initialLayout="@layout/widget_layout"
android:minWidth="40dp"
android:minHeight="40dp"
android:previewImage="@drawable/ic_launcher_foreground"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen">
</appwidget-provider>
๋ง๊ทธ๋๋ก ์์ฑ์ ๋ค ์ ์ด๋ ๊ฒ์ธ๋ฐ, ํํ๋ฉด์ ๋ฐฐ์นํ ์ ์๋ ์์ ฏ์ด๊ณ ๊ฐฑ์ ์ฃผ๊ธฐ๋ 24์๊ฐ์ด๋ค. (24*60*60*1000ms)
Android 12(21๋ 8์์ ์ ์ ์ถ์ ๋ OS ๋ฒ์ ์ด๋ผ ์๊ฐ๋ณด๋ค ์ต์ ์ด๋ค.)๋ถํฐ๋ Cell ๊ฐ๋ ์ผ๋ก ์์ ฏ์ ๋ฐฐ์นํ ์ ์๊ธฐ ๋๋ฌธ์ `targetCellWidth`, `targetCellHeight`๋ฅผ ์กฐ์ ํด์ ๋ช์นธ ์ฐจ์งํ๊ฒ ํ ๊ฑด์ง ๋ ๋ช ํํ๊ฒ ์ง์ ํ ์ ์๋ค. ์ฐธ๊ณ ๋ก ์ด Cell ๊ธฐ์ค์ด minWidth, Height ๋ณด๋ค ์ฐ์ ์์๊ฐ ๋์๋ฐ ์์ง 12 ๋ฐ ๋ฒ์ ๋ ์๋น์๋ผ ๊ผญ ๋๋ค ์ ์ธํด์ค์ผ๋๋ค.
๋คํฌ๋ชจ๋๊ฐ์ ์์คํ ์ค์ ์ ๋ฐ๋ผ๊ฐ๊ฒ ํ๋ ์์ฑ๋ ์๋ค.
android:widgetFeatures="widgetFeatures=reconfigurable|configuration_optional"
์์์ `AppWidgetProvider`๋ ๋ธ๋ก๋์บ์คํธ ๋ฆฌ์๋ฒ๋ฅผ ์์๋ฐ์ ํด๋์ค๋ผ๊ณ ํ๋ค. ๊ทธ ๋ง์ ๋งค๋ํ์คํธ ํ์ผ์ ๋ฆฌ์๋ฒ๋ฅผ ์ ์ธํด์ค์ผ๋๋ค๋ ๋ง๊ณผ ๋์ผํ๋ค.
<receiver android:name=".MyWidgetProvider" android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
๋ฐ๋ก ๋ง๋ค์ด๋ Provider ํด๋์ค๋ฅผ ์ง์ ํด์ receiver ์ ์ธ ํด์ค๋ค. ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ค์ ํ๋ info ๊ฐ์ฒด ์ญ์๊ฐ์ด ๋ฌ์์ค์ผํ๋ค.
์ด์ Provider ํด๋์ค๋ฅผ ๋ณด์.
private const val TAG = "MyWidgetProvider"
class MyWidgetProvider : AppWidgetProvider() {
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
}
override fun onUpdate(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetIds: IntArray?
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d(TAG, "onUpdate: ")
}
override fun onAppWidgetOptionsChanged(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
Log.d(TAG, "onAppWidgetOptionsChanged: ")
}
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
super.onDeleted(context, appWidgetIds)
Log.d(TAG, "onDeleted: ")
}
override fun onEnabled(context: Context?) {
super.onEnabled(context)
Log.d(TAG, "onEnabled: ")
}
override fun onDisabled(context: Context?) {
super.onDisabled(context)
Log.d(TAG, "onDisabled: ")
}
override fun onRestored(context: Context?, oldWidgetIds: IntArray?, newWidgetIds: IntArray?) {
super.onRestored(context, oldWidgetIds, newWidgetIds)
Log.d(TAG, "onRestored: ")
}
companion object {
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.widget_layout)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
์๋ช ์ฃผ๊ธฐ์ ๋ก๊ทธ๋ฅผ ์ฐ์ด๋ณด๊ณ ์ง์ ๊ฒฝํํด๋ณด๋ ์ฑ์ ์คํ๊ณผ ๋ ๋ฆฝ๋์ด ์คํ๋๋ ๊ฒ ์ ๊ธฐํ๋ค. ์ผ๋จ ํํ๋ฉด์ ์์ ฏ์ ์์ฑํ๊ณ ๋๋ฉด `onUpdate`๊ฐ ์์ ฏ ์ฌ์ด์ฆ ๋ณ๊ฒฝ๊ฐ์ ์ํฉ์์ ํธ์ถ๋๋ค. ์ ํํ๋ ์ ๋ฐ์ดํธ, ์ญ์ , ์ค์ , ์ค์ง์ ๊ฐ์ ๋ธ๋ก๋์บ์คํธ ์ด๋ฒคํธ๋ฅผ ์์ ํ๋ค๊ณ ๋ด์ผ๋๋ค.
์ฒ์ ์์ฑํ ๋๋ `onEnabled`๋ก ์์ํด์ -> `onUpdate` -> `onAppWidgetOptionsChanged` -> `onDeleted` -> `onDisabled` ๋ก ๊ตฌ์ฑ๋๋ค. `onReceive`๋ ๋ชจ๋ ๋ธ๋ก๋์บ์คํธ์ ์ ์ฝ๋ฐฑํจ์๋ค์ ํธ์ถํ๊ธฐ์ ์ ํธ์ถ๋๋๋ฐ ๋ฐ๋ก ๊ฑด๋๋ฆด ํ์๋ ์์ด๋ณด์ธ๋ค. ์ค์ํ๊ฑด ์ด ์ฝ๋ฐฑํจ์๋ค์ด ๋ฉ์ธ์ค๋ ๋์์ ์คํ๋ ๋ค๋ ์ ์ด๋ค. ๊ทธ๋์ ๋ฉ์ธ์ค๋ ๋์์ ํ๋ฉด ์๋๋ ์์ ๋ค์ ์์ ฏ์์๋ ํ๋ฉด ์๋๋ค.
Info ๊ฐ์ฒด์ updatePeriodMillis ์ฃผ๊ธฐ๋ง๋ค `onUpdate`๊ฐ ํธ์ถ๋๋ค. ์ต์๊ฐ์ด 30๋ถ์ผ๋ก ์ ํด์ ธ์๋ ๋ฏ ํ๊ณ , ์ด๊ฑฐ๋ณด๋ค ๋ ๋น ๋ฅธ ์ฃผ๊ธฐ๋ฅผ ์ํ๋ฉด WorkManager๋ฅผ ์ฌ์ฉํด์ผ๋ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค.
https://developer.android.com/develop/ui/views/appwidgets/advanced?hl=ko#periodic
onUpdate๊ฐ ์ฌ์ค ์์ ฏ์์ ๊ฐ์ฅ ์ค์ํ ์ฝ๋ฐฑํจ์๋ค. ๋ชจ๋ ์ํธ์์ฉ์ด ์ด ์ฝ๋ฐฑ์ ํ๊ธฐ ๋๋ฌธ์ธ๋ฐ, ๊ทธ๋์ DB์์ ๊ฐ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ด ์๋๊ฒฝ์ฐ์๋ ์ด ์ฝ๋ฐฑํจ์์์ ๋ค ์ฒ๋ฆฌํ๋ค๊ณ ํ๋ค.
์์ ฏ์ `RemoteView`์ ๊ธฐ๋ฐํ๋๋ฐ ์๊ฐ๋ณด๋ค ๋ง์ ๋ทฐ๋ค์ ์ ๊ณตํ๊ณ ์์ด(์ปค์คํ ๋ทฐ๋ ์๋๋ค.) ๋์ค์ ์ง์ ํ๋์ฉ ํด๋ณด๋ฉด ๋ ๊ฒ ๊ฐ๋ค. ๋๋ ์ผ๋จ FrameLayout์ผ๋ก ์ก๊ณ ํ ์คํธ๋ทฐ๋ง ํ๋ ๋ฃ์ด๋์ ์์ง RemoteView๋ก ๋ญ ํ ์ ์๋ ์ง ๊ฐ์ด ์์จ๋ค.
์ง์๋๋ ๋ ์ด์์, ๋ทฐ๋ค์ด๋ค.
# ์์น์ ๋ณด๋ ์ฎ์ด๋ณด๋ ค๋ฉด?
์์น๊ธฐ๋ฐ ์๋น์ค์ ์ผ๋ถ๋ถ์ผ๋ก ์์ ฏ์ ์๊ฐํ๋ ๊ฒ์ด๋ผ์ ์ด๊ฒ ์ ์ผ ์ค์ํ๋ค.
onUpdate์์ ์ฒ๋ฆฌํ๋ฉด ๋๋ค๊ณ ๋ ํ์ง๋ง, ๋ธ๋ก๋์บ์คํธ ๋ฆฌ์๋ฒ๋ฅผ ์์๋ฐ์ ํน์ง์ด ๊ทธ๋๋ก ์ด์์์ด์ onUpdate๊ฐ ํธ์ถ๋๊ณ ๋์ ํ๋ก์ธ์ค๊ฐ ์ด์์๋ค๋ ๋ณด์ฅ์ด์๋ค.
๋๊ธด ๋๋๋ฐ ์ธํฐ๋ฒ(์ฝ 10๋ถ)์ด ์ข ์ฌํ๊ณ ๊ธฐ์ค์ด ๋ญ์ง๋ฅผ ๋ชจ๋ฅด๊ฒ ๋ค.
์ด ๋ถ๋ถ ๋๋ฌธ์ ์์ฒญ์ด ์์ฒญ ๋ฆ๋ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค... `FLP`๋ก ์ ํํ๋ ๋ฐฉ๋ฒ์ ๋ ์ฐพ์๋ด์ผ๊ฒ ๋ค.
private const val TAG = "MyWidgetProvider"
class MyWidgetProvider : AppWidgetProvider() {
private lateinit var fusedLocationClient: FusedLocationProviderClient
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
}
override fun onUpdate(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetIds: IntArray?
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d(TAG, "onUpdate: ")
fusedLocationClient = LocationServices.getFusedLocationProviderClient(context!!)
val locationRequest = LocationRequest.create().apply {
interval = 1000
fastestInterval = 500
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location ->
// ์์น ์ ๋ณด๋ฅผ ์ฌ์ฉํ์ฌ ์์ ฏ ์
๋ฐ์ดํธ
Log.d(TAG, "onLocationResult: ${location}")
}
}
}
// ์์น ์
๋ฐ์ดํธ ์์ฒญ
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
}
}
override fun onAppWidgetOptionsChanged(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
Log.d(TAG, "onAppWidgetOptionsChanged: ")
}
companion object {
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.widget_layout)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
๋์์ด ๋๋ค๋ฉด ๋๊ธ์ด๋ ๊ณต๊ฐ ๋ฒํผ ํ ๋ฒ์ฉ ๋๋ฅด๊ณ ๊ฐ์ฃผ์ธ์!