프로그래밍 놀이터/안드로이드, Java

[android] 다시 살짝 정리해보는 AppWidget.

돼지왕 왕돼지 2023. 8. 18. 20:08
반응형


https://developer.android.com/develop/ui/views/appwidgets

#
AppWidgetHost
    위젯을 로드하고 표시할 앱. 보통 Launcher 가 그 역할을 함.
AppWidgetProviderInfo
    widget 의 layout, update frequency, AppWidgetProvider 등의 metadata 기술 xml 형태로 제공함
AppWidgetProvider
    widget 의 lifecycle 관련된 method 들을 정의. widget 이 update, enable, disable, deleted 되었을 때 event 를 broadcast 로 받을 수 있음.
View layout
    xml 로 정의

 

#
Android Studio 에서는 New > Widget > App Widget 을 통해 AppWidgetProviderInfo, AppWidgetProvider, view layout file 의 생성을 지원함.

 

#
Widget 에 configuration activity 를 지원할 수도 있음.
Android 12 (API Level 31) 부터는 default configuration 을 제공할 수 있으며, user 가 widget 을 나중에 재설정 할 수 있음.
Android 11 (API Level 30) 이하에서는 home screen 에 widget 이 추가될 때마다 항상 launch 됨.

 

 

<AppWidgetProviderInfo>

#
AppWidgetProviderInfo XML 은 res/xml/ 에 정의하며 예시는 아래와 같음.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:targetCellWidth="1"
    android:targetCellHeight="1"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="120dp"
    android:updatePeriodMillis="86400000"
    android:description="@string/example_appwidget_description"
    android:previewLayout="@layout/example_appwidget_preview"
    android:initialLayout="@layout/example_loading_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen"
    android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>

 

#
대부분의 런처는 정수값의 cell 지정을 지원함.
기본 사이즈, 위젯 최소 최대값 등을 설정할 수 있음.

 

#
targetCellWidth, targetCellHeight 는 Android 12 부터 사용할 수 있으며, grid cell 기준 기본 사이즈를 지정함. Android 11 이하에서는 홈스크린이 grid-based layout 을 지원하지 않으면 무시되는 값임.
minWidth, minHeight 는 dp 값으로 위젯 기본 사이즈를 지정. 만약 이 값이 cell 값과 정확히 맞지 않으면 반올림한 값에 가까운 cell count 가 적용됨.

targetCellWidth/Height 와 minWidth/Height 가 동시에 지정하는 것이 좋음.
targetCellWidth/Height 가 지원되는 경우는 이 값이 먼저 쓰이고, 그렇지 않으면 minWidth/Height 값이 차순으로 사용됨.

 

#
minResizeWidth, minResizeHeight 는 위젯의 절대적인 min size 를 지정함.
이 값은 위젯이 정말 지원할 수 없는 수준의 값이 지정되어야 함.
minResizeWidth > minWidth 인 경우 minResizeWidth 값은 무시됨.
horizontal resizing 이 disable 된 경우에도 이 값은 무시됨.
minResizeHeight 도 마찬가지 로직을 따름.

 

#
maxResizeWidth, maxResizeHeight 는 위젯의 권장되는 max size 를 지정함.
Android 12 에 도입됨.
grid cell 의 배수값이 아니라면 반올림해서 맞는 cell 값으로 할당됨.
maxResizeWidth < minWidth 인 경우 maxResizeWidth 값은 무시됨.
horizontal resizing 이 disable 된 경우에도 이 값은 무시됨.
maxResizeWidth 도 마찬가지 로직을 따름.

 

#
updatePeriodMillis 는 widget framework 이 얼마나 자주 AppWidgetProvider 의 onUpdate() 를 호출할지를 지정함.
그러나 이 호출주기는 정확히 보장되지 않으며, 최대한 자동갱신을 하지 않는 것이 권장됨.
1시간에 1회 초과는 배터리 절약 차원에서 비추됨.

 

#
initialLayout 은 widget layout 을 지정함

 

#
configure 는 user 가 widget 을 추가했을 때 불리는 activity 를 정의하며 widget 의 property 들을 설정함.
Android 12 부터는 이 initial config 를 skip 할 수 있음.

 

#
description 은 Android 12 에서 등장했으며, widget picker 에 표시될 description 을 지정함.

 

#
previewLayout (Android 12), previewImage(Android 11 이하).
previewLayout 은 Android 12 부터 지원하며, scale 가능한 preview 를 지정하며, 이는 기본 사이즈의 위젯에 대한 xml layout 을 지정한다.
previewImage 는 Android 11 이하에서 지원하며, 위젯이 설정된 후 어떻게 보일지의 preview 를 지정함. 제공되지 않으면 유저는 앱의 launcher icon 을 보게 됨.
previewLayout, previewImage 를 둘 다 지원하는 것이 권장됨. fallback 효과를 위해.

 

#
autoAdvanceViewId 는 widget host 에 의해 자동으로 다음 item 을 보여줄 widget subview 의 view ID 를 명시함.
예를 들어 StackView 를 사용하고 이 설정을 한 경우, 자동으로 다음 item 을 순환하며 보여줌

 

#
widgetCategory 는 위젯이 home screen(home_screen), lock screen(keyguard), 혹은 양쪽 다 노출되어야 하는지를 지정함. Android version 5.0 미만에서만 lock-screen widget 이 지원되고, 이상에서는 home_screen 만 유효함.

 

#
widgetFeatures 는 widget 에 의해 지원되는 기능을 명시함.
유저가 위젯 추가했을 때 기본 설정을 사용하게 하려면 configuration_optional 와 reconfigurable flag 를 설정해 줄 수 있음. 이렇게 하면 유저가 위젯을 추가했을 때 바로 configuration activity 를 호출하는 것을 피할 수 있고, 나중에 재설정 할 수 있음.

 

 

<AppWidgetProvider>

#
AppWidgetProvider 는 widget 의 broadcast 를 처리하고, widget 의 lifecycle 에 따라 update 를 하는 역할을 함.

 

#
AppWidgetProvider 의 manifest 정의는 아래와 같은 형태로 함.

<receiver android:name="ExampleAppWidgetProvider"
                 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/example_appwidget_info" />
</receiver>

 

#
다른 외부 broadcast 를 받을 것이 아니라면 exported 는 false 가 되어야 함.
유일하게 꼭 받아야 하는 broadcast 는 APPWIDGET_UPDATE 이며, 이 broadcast 를 보내는 주체는 AppWidgetManager.
meta-data 에는 AppWidgetProviderInfo res 를 연결함.

 

#
AppWidgetProvider 는 BroadcastReceiver 를 상속하며, widget broadcast 관리를 위한 편리한 기능을 제공함.
widget 이 updated, deleted, enabled, disabled 되었는지를 event 로 받음.

 

#
onUpdate()
    AppWidgetProviderInfo 에 정의한 updatePeriodMillis 에 지정한 시간이 되었을 때 호출됨.
    updatePeriodMillis 는 위젯이 여러개일 때 첫번째 녀석의 시간주기에 따름. 예를 들어 시간주기가 2시간이고, 첫번재 위젯 추가 후 두번째 위젯을 1시간 후 추가하면, 첫번재 위젯 추가 후 2시간 후에 한번만 callback 이 불리고, 두번째 위젯의 시간주기는 무시됨. (즉 매시간 불리지 않음)

    유저가 widget 을 추가했을 때도 필수 설정 할 기회를 주기 위해 불려짐. (view 설정, 표시 data loading 을 위한 job 시작 등을 할 수 있음)
    configuration_optional 을 지정하지 않아 widget 생성 시 configuration activity 가 호출되는 경우에 이 함수는 불리지 않고, 이후 update 씬에서는 불림. 따라서 configuration activity 에서 first update 를 끝마칠 책임이 주어짐.
    이 함수는 가장 중요한 callback 임.

    AppWidgetProvider 가 BroadcastReceiver 구현체이기 때문에 해당 함수의 실행 완료가 보장되지 않음. 오래 걸리는 작업을 해야 하는 경우 WorkManager 를 사용하는 것이 권장됨. 그렇지 않으면 ANR 을 맞을 수 있음.

class ExampleAppWidgetProvider : AppWidgetProvider() {

    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
    ) {
        // Perform this loop procedure for each widget that belongs to this provider.
        appWidgetIds.forEach { appWidgetId ->
            // Create an Intent to launch ExampleActivity.
            val pendingIntent: PendingIntent = PendingIntent.getActivity(
                    /* context = */ context,
                    /* requestCode = */  0,
                    /* intent = */ Intent(context, ExampleActivity::class.java),
                    /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )

            // Get the layout for the widget and attach an on-click listener to the button
            val views: RemoteViews = RemoteViews(
                    context.packageName,
                    R.layout.appwidget_provider_layout
            ).apply {
                setOnClickPendingIntent(R.id.button, pendingIntent)
            }

            // Tell the AppWidgetManager to perform an update on the current widget.
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

 

#
onAppWidgetOptionsChanged()
    위젯이 처음 설치되거나, resize 될 때마다 불힘. 이 callback 이 불렸을 때 보여줄 contents 를 show/hide 하면 됨. getAppWidgetOptions() 를 통해 받아온 Bundle 을 통해 widget size 를 알 수 있음.
    OPTION_APPWIDGET_MIN_WIDTH, OPTION_APPWIDGET_MIN_HEIGHT, OPTION_APPWIDGET_MAX_WIDTH, OPTION_APPWIDGET_MAX_HEIGHT, OPTION_APPWIDGET_SIZES (Android 12) 등의 key 로 값을 조회해 봄면 됨.

 

#
onDeleted(Context, int[])
    widget 이 삭제되었을 때마다 불림

 

#
onEnabled(Context)
    위젯이 Host 에 첫번째로 설치되었을 때 불림. 예를 들어 유저가 2개의 widget 을 설치하는 경우 첫번째 위젯을 설치할 때는 불리고, 두번째 위젯을 설치할 때는 불리지 않음.

 

#
onDisabled(Context)
    마지막 위젯이 Host 에서 제거되었을 때 불림. onEnabled 에서 작업한 내용을 클리어 하기 좋은 위치

 

#
onReceive(Context, Intent)
    callback method 들을 호출하기 전에 근원적으로 불리는 broadcaast receiver callback method. 일반적으로 이 함수를 구현할 필요가 없음. 기본적으로 알아서 parsing 해서 필요한 함수들을 불러주기 떄문.

 

 

<Widget Layout>

#
Widget Layout 들은 RemoteViews 기반으로 작동함.
RemoteViews 는 ViewStub 도 제공함.

 

#
Android 12 에서는 CheckBox, Switch, RadioButton 을 제공함.
사실상 이 버튼들은 stateless 이기 때문에 앱이 이 상태를 저장해야 함.

// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true)

// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2)

// Listen for check changes. The intent will have an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
        R.id.my_checkbox,
        RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent)
)

 

Android 12 에서 새로 지원하는 것들이 있기 때문에, res/layout-v31 과 그 이전버전을 위한 res/layout 두 세트를 지원하는 것이 좋음.

 

#
Android 12 에서는 위젯의 rounded corner 가 자동으로 적용됨.
system_app_widget_background_radius, system_app_widget_inner_radius 라는 2개의 system 값을 사용할 수 있음. 전자는 widget root container 의 rounded corner radius 값으로 사용하고, 후자는 inner view 의 rounded corner radius 값으로 사용하면 좋음.

가장 간단한 사용방법은 아래와 같이 shape 을 만들어 widget layout 의 배경으로 지정하는 것.

// res/drawable/app_widget_background.xml
<shape android:shape="rectangle">
    <corners android:radius="@android:dimen/system_app_widget_background_radius">
    …
</shape>

// res/drawable/app_widget_inner_view_background.xml
<shape android:shape="rectangle">
    <corners android:radius="@android:dimen/system_app_widget_inner_radius">
    …
</shape>

// res/layout/widget_layout.xml
<LinearLayout
    android:background="@drawable/app_widget_background"
…>
    <LinearLayout
        android:background="@drawable/app_widget_inner_view_background"
    …>
    </LinearLayout>
</LinearLayout>

system_app_widget_background_radius : 28dp 이상이 될 수 없음.
system_app_widget_inner_radius : system_app_widget_background_radius 보다 8dp 적은 값. (8dp padding 과 잘 어울림)

3rd party launcher 나 제조사가 위 값들을 override 할 수 있으므로 상황마다 값이 다를 수 있음.

위젯이 @android:id/background 를 사용하지 않거나,
android:clipToOutline=true 등을 사용하지 않는다면 launcher 는 bg 를 알아서 감지하고 사각형 widget 을 16dp 까지 rounded corner 로 만들 수 있음.

이 역시 Android 12 에 도입된 것이 때문에 Style 을 통한 분기가 추천됨.

 

#
Android 12 에서는 다양한 기능들이 추가됨.
Android 12 에서만 지원하는 기능들은 하위 호환성을 위해 style 등을 2벌 정의하는 것이 추천됨.

Dynamic colors
    device theme color 를 btn, bg 등에 적용할 수 있음.
    dynamic color 를 사용하기 위해서는 root layout 에 system default theme 인 @android:style/Theme.DeviceDefault.DayNight 또는 Material 3 theme 인 Theme.Material3.DynamicColors.DayNight 을 적용해야 함.
    이후 적용하고 싶은 다음 색상들을 지정해줌면 됨. ?android:attr/colorAccent, ?android:attr/colorBackground, ?android:attr/textColorPrimary, ?android:attr/textColorSecondary

 

Voice support
    Built-in intents(BII) 라는 것이 있음. 유저의 voice command 에 의해 해당 Intent 가 발생하면 assistant 에 widget 을 보여줄 수 있음.
    자세한 사항은 관련 내용을 참조.. (https://developer.android.com/develop/ui/views/appwidgets/enhance#voice)

 

Improve widget picker experience
    Widget picker 를 scaleable 하게 할 수 있음. 기존 widget preview 가 static image 였다면 이제는 dynamic layout 가능한 xml 을 제공. 이를 통해 home screen(host)에서 어떻게 보일지를 더 잘 알 수 있게 됨.
    appwidget-provider 의 android:previewLayout 에 값을 지정해줌면 됨.
    일반적으로 previewLayout 과 initialLayout 을 동일하게 사용하며, 이것이 권장됨.
    하위 호환성을 위해 previewLayout 과 previewImage 모두 설정하는 것이 좋음.

 

Description of widget
    appwidget-provider 의 android:description 에 설명을 넣을 수 있음.
    글자수 제한은 없지만 단말에 따라 잘려 보일 수 있기 때문에 간단하게 쓰는 것이 추천됨.

 

Enable smoother transitions
    widget 에서 app 을 실행시킬 때 부드러운 transition 을 제공함
    이를 위해 top level layout 에 @android:id/background 나 android.R.id.background 를 설정하는 것이 추천됨.

 

Runtime modification of RemoteViews

// Set the colors of a progress bar at runtime.
remoteView.setColorStateList(R.id.progress, "setProgressTintList", createProgressColorStateList())

// Specify exact sizes for margins.
remoteView.setViewLayoutMargin(R.id.text, RemoteViews.MARGIN_END, 8f, TypedValue.COMPLEX_UNIT_DP)

 

 

<Advanced widget>

#
Widget content update 는 무거운 작업임. 배터리 절약 차원에서 update 타이밍, 주기, 종류 등을 세심히 볼 필요가 있음.

 

#
Type of widget updates
    Full update
        AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) 는 위젯 전체를 업데이트 시킴. 가장 무거운 작업임.

 

    Partial update
        AppWidgetManager.partiallyUpdateAppWidget(int, android.widget.RemoteViews) 는 일부만 업데이트함. 기존 RemoteViews 를 새로운 RemoteViews 로 치환함.
        partial update 를 사용하려면 최소한 한번 full update 가 불렸어야 함.

 

    Collection data refresh
        AppWidgetManager.notifyAppWidgetViewDataChanged 는 RemoteViewsFactory.onDataSetChanged 의 호출을 야기하여 최종적으로 collection data 를 갱신함.

AppWidgetProvider 와 같은 UID 를 갖는 app 어디에서든 이 함수들을 호출할 수 있음.

 

#
Frequency of update
    Periodically
        updatePeriodMillis 값에 따라 주기적으로 update 할 수 있고, user interaction, broadcast update 등에 의해서도 update 될 수 있음.
        updatePeriodMillis 조건이 충족되면 AppWidgetProvider.onUpdate 함수가 불림. 그러나 이곳에서 오래 걸리는 작업을 수행한다면 다른 방식을 생각해봐야 함. 기본적으로 AppWidgetProvider 는 BroadcastReceiver 의 구현체이기 때문에 10초 이상 thread 를 잡고 있으면 ANR 이 발생함. WorkManager 를 사용하는 것이 좋음

        updatePeriodMillis 는 30분 미만으로 설정할 수 없음. periodic update 가 필요하지 않으면 0으로 설정할 수도 있음.


    User interaction
        app 의 activity 에서는 AppWidgetManager.updateAppWidget 을 호출할 수 있음.
        notification, app widget 같은 remote interaction 에서는 PendingIntent 를 사용하고 이를 통해 시작된 Activity, Broadcast, Service 에서 어떤 처리를 하면 됨.

 

    Broadcast
        예를 들어 유저가 사진을 찍었을 때 위젯을 업데이트 하고 싶다면, addTriggerContentUri 조건을 걸어서 JobScheduler 를 실행시키는 방법이 있음.
        ACTION_LOCALE_CHANGED 와 같은 event 를 받을 니즈가 있을 수도 있음.

        Broadcast 의 기본 ANR 시간은 10초인데, goAsync 함수를 통해 30초까지 할당받을 수 있음.
        기본적으로 broadcast 는 bg process 에서 동작함. 그래서 시스템이 바쁠 때는 broadcast 를 받는데 시간이 걸릴 수 있음. 이 경우 Intent.FLAG_RECEIVER_FOREGROUND 를 Intent 에 추가해서 fg process 로 승격(?) 시킬 수 있음.

 

#
Collection 사용하는 Preview 는 empty item 들을 노출하기 쉬움.
그래서 initialLayout 과 동일한 파일 대신, preview 전용 layout 을 따로 사용하는 것이 추천됨.

 

 

<Collection widgets>

#
Collection widget 은 RemoteViewsService 를 사용해서 ContentProvider 등을 통해 data 를 불러옴.
RemoteViewService 는 RemoteAdapter 가 RemoteViews 를 요청할 때 사용함.

 

#
ListView, GridView, StackView, AdapterViewFlipper 가 대표적인 collection view type.
Widget 에서는 Adapter 의 역할을 RemoteViewFactory 가 수행하는데, 이는 Adapter interface 를 wrapping 한 녀석. RemoteViewsFactory 가 collection item 을 RemoteViews 형태로 전달해줌.

 

#
https://android.googlesource.com/platform/development/+/master/samples/StackWidget 에서 사용방법을 볼 수 있음.

 

#
User interaction 없이 widget 이 자동으로 순서대로 다음 view 를 보여줄 수 있는데,
이는 autoAdvanceViewId 에 stack view 를 지정해주면 됨.

 

#
RemoteViewsService 를 manifest 에 정의할 때는 BIND_REMOTEVIEWS permission 을 지정해주어야 함.

<service android:name="MyWidgetService"
    android:permission="android.permission.BIND_REMOTEVIEWS" />

 

#
AppWidgetProvider.onUpdate() 에서 위젯 생성 시 setRemoteAdapter() 를 호출해주어 어디서부터 data 를 가져올지 알려줘야 함.

override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
) {
    // Update each of the widgets with the remote adapter.
    appWidgetIds.forEach { appWidgetId ->
        // Set up the intent that starts the StackViewService, which provides the views for this collection.
        val intent = Intent(context, StackWidgetService::class.java).apply {
            // Add the widget ID to the intent extras.
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
            data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
        }

        // Instantiate the RemoteViews object for the widget layout.
        val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
            // Set up the RemoteViews object to use a RemoteViews adapter.
            // This adapter connects to a RemoteViewsService through the specified intent.
            // This is how you populate the data.
            setRemoteAdapter(R.id.stack_view, intent)

            // The empty view is displayed when the collection has no items.
            // It must be in the same layout used to instantiate the RemoteViews object.
            setEmptyView(R.id.stack_view, R.id.empty_view)
        }

        // Do additional processing specific to this widget.
        appWidgetManager.updateAppWidget(appWidgetId, views)
    }
    super.onUpdate(context, appWidgetManager, appWidgetIds)
}

 

#
static data 가 아니면 RemoteViewsService 에 member 로 들고 있지 말자.
ContentProvider 를 통해 자료를 주고 받는 것이 좋다.

 

#
RemoteViewFactory 에서 구현해야 하는 가장 중요한 함수는 onCreate 와 getViewAt 이다.
시스템은 factory 를 처음 만들 때 onCreate 를 호출함. 이 때가 data source 와 연결하는 타이밍이고, widget 이 active 되었을 때 이 data source 를 이용하여 정보를 표시함.

private const val REMOTE_VIEW_COUNT: Int = 10

class StackRemoteViewsFactory(
        private val context: Context
) : RemoteViewsService.RemoteViewsFactory {

    private lateinit var widgetItems: List<WidgetItem>

    override fun onCreate() {
         // In onCreate(), set up any connections or cursors to your data source. 
	// Heavy lifting, such as downloading or creating content, must be deferred to onDataSetChanged() or getViewAt(). 
	// Taking more than 20 seconds on this call results in an ANR.
        widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") }
        ...
    }
    ...
}

getViewAt 은 RemoteViews 를 return 함.

override fun getViewAt(position: Int): RemoteViews {
    // Construct a remote views item based on the widget item XML file and set the text based on the position.
    return RemoteViews(context.packageName, R.layout.widget_item).apply {
        setTextViewText(R.id.widget_item, widgetItems[position].text)
    }
}

 

#
setOnClickPendingIntent() 함수는 collection 쪽에서는 쓸 수 없음.
collection 에서는 setPendingIntentTemplate 와 setOnClickFillInIntent 조합을 사용해야 함.
보통은 바로 Activity 를 띄우기도 하지만, widget 내에서 다른 처리가 필요하다면 Broadcast 를 통해 통신할 수 있음.

const val TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION"
const val EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM"

class StackWidgetProvider : AppWidgetProvider() {

    ...

    // Called when the BroadcastReceiver receives an Intent broadcast.
    // Checks whether the intent action is TOAST_ACTION. If it is, the widget displays a Toast message for the current item.
    override fun onReceive(context: Context, intent: Intent) {
        val mgr: AppWidgetManager = AppWidgetManager.getInstance(context)
        if (intent.action == TOAST_ACTION) {
            val appWidgetId: Int = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID
            )
            // EXTRA_ITEM represents a custom value provided by the Intent  passed to the setOnClickFillInIntent() method to indicate the position of the clicked item. 
	  // See StackRemoteViewsFactory in Set the fill-in Intent for details.
            val viewIndex: Int = intent.getIntExtra(EXTRA_ITEM, 0)
            Toast.makeText(context, "Touched view $viewIndex", Toast.LENGTH_SHORT).show()
        }
        super.onReceive(context, intent)
    }

    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
    ) {
        // Update each of the widgets with the remote adapter.
        appWidgetIds.forEach { appWidgetId ->
	  ...

            // This section makes it possible for items to have individualized behavior. 
	  // It does this by setting up a pending intent template.
            // Individuals items of a collection cannot set up their own pending intents. 
	  // Instead, the collection as a whole sets up a pending intent template, and the individual items set a fillInIntent to create unique behavior on an item-by-item basis.
            val toastPendingIntent: PendingIntent = Intent(
                    context,
                    StackWidgetProvider::class.java
            ).run {
                // Set the action for the intent.
                // When the user touches a particular view, it has the effect of broadcasting TOAST_ACTION.
                action = TOAST_ACTION
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))

                PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT)
            }
            rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent)

            appWidgetManager.updateAppWidget(appWidgetId, rv)
        }
        super.onUpdate(context, appWidgetManager, appWidgetIds)
    }
}

private const val REMOTE_VIEW_COUNT: Int = 10

class StackRemoteViewsFactory(
        private val context: Context,
        intent: Intent
) : RemoteViewsService.RemoteViewsFactory {
    ...

    override fun getViewAt(position: Int): RemoteViews {
        // Construct a remote views item based on the widget item XML file
        // and set the text based on the position.
        return RemoteViews(context.packageName, R.layout.widget_item).apply {
            setTextViewText(R.id.widget_item, widgetItems[position].text)

            // Set a fill-intent to fill in the pending intent template.
            // that is set on the collection view in StackWidgetProvider.
            val fillInIntent = Intent().apply {
                Bundle().also { extras ->
                    extras.putInt(EXTRA_ITEM, position)
                    putExtras(extras)
                }
            }
            // Make it possible to distinguish the individual on-click action of a given item.
            setOnClickFillInIntent(R.id.widget_item, fillInIntent)
            ...
        }
    }
    ...
}

 

#
data 를 계속 최신으로 유지하는 과정은 아래의 과정으로 이루어진다.

  1. RemoteViews.setRemoteViewsAdapter()
  2. AppWidgetManager.updateAppWidget()
  3. RemoteViewFactory.onCreate()
  4. AppWidgetManager.notifyAppWidgetViewDataChanged() -> RemoteViewFactory.onDataSetChanged()
  5. RemoteViewFactory.getCount(), getViewTypeCount(), hasStableIds(), getLoadingView()
  6. RemoteViewFactory.getViewAt(), getItemId()

 

#
getViewAt() 이 오래 걸린다면, RemoteViewsFactory.getLoadingView() 가 불리면서 loading view 가 먼저 표시된다.

 

#
Android 12 (API Level 31) 에서는 setRemoteAdapter(int viewId, RemoteViews.RemoteCollectionItems items) 가 추가되어, 기존과 같이 service 를 거치지 않고 collection view 를 직접 전달할 수 있다.
이를 사용할 경우 RemoteViewsFactory 에서 notifyAppWidgetViewDataChanged() 를 구현할 필요가 없다.
덧붙여 새로운 item 을 가져올 때의 latency 도 줄일 수 있다.
그러나 이 방법은 많은 Bitmaps 를 사용하여 setImageViewBitmap 함수 호출로 이어지는 경우 사용하기 어렵다.

 

#
Valid 한 구현인지 검증은 안 되었지만 아래와 같은 형태로 쓸 수 있는듯하다

// Get the app widget manager
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

// Get the app widget IDs for your app widget
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, YourAppWidgetProvider.class));

// Iterate over the app widget IDs
for (int appWidgetId : appWidgetIds) {
    // Create a RemoteViews object for your app widget
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.your_app_widget_layout);

    // Set the remote adapter for the specified viewId
    remoteViews.setRemoteAdapter(R.id.your_collection_view, items);

    // Update the app widget
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
}

 

#
만약 collection 이 동일한 layout 을 사용하지 않는다면, setViewTypeCount 를 사용할 수 있다.

val itemLayouts = listOf(
        R.layout.item_type_1,
        R.layout.item_type_2,
        ...
)

remoteView.setRemoteAdapter(
        R.id.list_view,
        RemoteViews.RemoteCollectionItems.Builder()
            .addItem(/* id= */ ID_1, RemoteViews(context.packageName, R.layout.item_type_1))
            .addItem(/* id= */ ID_2, RemoteViews(context.packageName, R.layout.item_type_2))
            ...
            .setViewTypeCount(itemLayouts.count())
            .build()
)

 

 

<Size your widget>

#
따로 정리하지 않는다.

 

 

<Configuration activity>

#
App Widget host 에 의해 위젯 최초 생성시 자동으로 뜨거나 혹은 나중에 띄울 수 있음. (configuration options 에 따름)

 

#
Config Activity 정의할 때는 intent-filter action 으로 android.appwidget.action.APPWIDGET_CONFIGURATION 을 설정해주고,
AppWidgetProviderInfo 의 android:configure 에 full name 으로 명시해주어야 함. (외부에서 호출하기 때문)

 

#
configuration activity 는 result 를 return 해야 하는데, 여기는 config activity 를 launch 할 때 EXTRA_APPWIDGET_ID 로 전달받은 app widget id 를 넣어주어야 함.
RESULT_CANCELED 를 return 하는 경우 app widget host 는 widget 을 추가하지 않음.
updateAppWidget 을 통해 app widget view 를 update 한 경우 RESULT_OK 를 return 하면 성공적으로 widget 이 추가됨.

 

#
System 은 config activity 가 launch 된 경우 ACTION_APPWIDGET_UPDATE broadcast 를 날리지 않음.
이 말은 위젯이 붙었을 때 onUpdate 가 불리지 않을 것이라는 의미.
widget 을 update 하는 것은 config activity 가 해야 하며, AppWidgetManager 를 통해서 수행함.

 

#
AppWidgetProviderInfo 에 android:widgetFeatures 를 reconfigurable 을 정의하면
long press 로 widget 이 edit mode 로 들어갔을 때, reconfig button 이 생기고 이것을 누르면 진입할 수 있음.
이 설정은 Android 9 (API Level 8) 에 도입되었지만, Android 12 까지 잘 지원되지 않음.

 

#
user 가 initial config 를 스킵하도록 하려면, android:widgetFeatures 에 configuration_optional | reconfigurable 을 지정해주면, 최초 위젯이 붙을 때는 설정 화면에 들어가지 않고 나중에 진입할 수 있음.
이 기능은 Android 12 부터 지원됨.

 

#
Android 8 (API Level 26) 이상부터는 pinned shortcut 이 지원되고, 이와 비슷한 개념으로 pinned widget 도 제공됨.
이는 앱에서 바로 home screen 에 widget 을 추가하는 것을 말함.
예를 들면 날씨 앱의 앱 내에서 새로운 도시를 관심 도시로 추가하면, 유저에게 해당 도시의 날씨 위젯을 추가할지 물어보고 추가할 수 있음.

val appWidgetManager = AppWidgetManager.getInstance(context)
val myProvider = ComponentName(context, ExampleAppWidgetProvider::class.java)

if (appWidgetManager.isRequestPinAppWidgetSupported()) {
    // Create the PendingIntent object only if your app needs to be notified when the user chooses to pin the widget. 
    // Note that if the pinning operation fails, your app isn't notified. 
    // This callback receives the ID of the newly pinned widget (EXTRA_APPWIDGET_ID).
    val successCallback = PendingIntent.getBroadcast(
            /* context = */ context,
            /* requestCode = */ 0,
            /* intent = */ Intent(...),
            /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT)
    appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
}

 

 

반응형