1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.widget
18 
19 import android.content.Context
20 import com.android.launcher3.BuildConfig
21 import com.android.launcher3.Launcher
22 import com.android.launcher3.LauncherAppState
23 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
24 import com.android.launcher3.logging.FileLog
25 import com.android.launcher3.model.data.LauncherAppWidgetInfo
26 import com.android.launcher3.qsb.QsbContainerView
27 
28 /** Utility class for handling widget inflation taking into account all the restore state updates */
29 class WidgetInflater(private val context: Context) {
30 
31     private val widgetHelper = WidgetManagerHelper(context)
32 
inflateAppWidgetnull33     fun inflateAppWidget(item: LauncherAppWidgetInfo): InflationResult {
34         if (item.hasOptionFlag(LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET)) {
35             item.providerName = QsbContainerView.getSearchComponentName(context)
36             if (item.providerName == null) {
37                 return InflationResult(
38                     TYPE_DELETE,
39                     reason = "search widget removed because search component cannot be found",
40                     restoreErrorType = RestoreError.NO_SEARCH_WIDGET,
41                 )
42             }
43         }
44         if (LauncherAppState.INSTANCE.get(context).isSafeModeEnabled) {
45             return InflationResult(TYPE_PENDING)
46         }
47         val appWidgetInfo: LauncherAppWidgetProviderInfo?
48         var removalReason = ""
49         @RestoreError var logReason = RestoreError.OTHER_WIDGET_INFLATION_FAIL
50         var update = false
51 
52         if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
53             // The widget id is not valid. Try to find the widget based on the provider info.
54             appWidgetInfo = widgetHelper.findProvider(item.providerName, item.user)
55             if (appWidgetInfo == null) {
56                 if (!BuildConfig.WIDGETS_ENABLED) {
57                     removalReason = "widgets are disabled on go device."
58                     logReason = RestoreError.WIDGETS_DISABLED
59                 } else {
60                     removalReason = "WidgetManagerHelper cannot find a provider from provider info."
61                     logReason = RestoreError.MISSING_WIDGET_PROVIDER
62                 }
63             } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) {
64                 // since appWidgetInfo is not null anymore, update the provider status
65                 item.restoreStatus =
66                     item.restoreStatus and LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY.inv()
67                 update = true
68             }
69         } else {
70             appWidgetInfo =
71                 widgetHelper.getLauncherAppWidgetInfo(item.appWidgetId, item.targetComponent)
72             if (appWidgetInfo == null) {
73                 if (item.appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
74                     removalReason = "CustomWidgetManager cannot find provider from that widget id."
75                     logReason = RestoreError.INVALID_CUSTOM_WIDGET_ID
76                 } else {
77                     removalReason =
78                         ("AppWidgetManager cannot find provider for that widget id." +
79                             " It could be because AppWidgetService is not available, or the" +
80                             " appWidgetId has not been bound to a the provider yet, or you" +
81                             " don't have access to that appWidgetId.")
82                     logReason = RestoreError.INVALID_WIDGET_ID
83                 }
84             }
85         }
86 
87         // If the provider is ready, but the widget is not yet restored, try to restore it.
88         if (
89             !item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) &&
90                 item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED
91         ) {
92             if (appWidgetInfo == null) {
93                 return InflationResult(
94                     type = TYPE_DELETE,
95                     reason =
96                         "Removing restored widget: id=${item.appWidgetId} belongs to component ${item.providerName} user ${item.user}, as the provider is null and $removalReason",
97                     restoreErrorType = logReason,
98                 )
99             }
100 
101             // If we do not have a valid id, try to bind an id.
102             if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
103                 if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
104                     // Id has not been allocated yet. Allocate a new id.
105                     LauncherWidgetHolder.newInstance(context).let {
106                         item.appWidgetId = it.allocateAppWidgetId()
107                         it.destroy()
108                     }
109                     item.restoreStatus =
110                         item.restoreStatus or LauncherAppWidgetInfo.FLAG_ID_ALLOCATED
111 
112                     // Also try to bind the widget. If the bind fails, the user will be shown
113                     // a click to setup UI, which will ask for the bind permission.
114                     val pendingInfo = PendingAddWidgetInfo(appWidgetInfo, item.sourceContainer)
115                     pendingInfo.spanX = item.spanX
116                     pendingInfo.spanY = item.spanY
117                     pendingInfo.minSpanX = item.minSpanX
118                     pendingInfo.minSpanY = item.minSpanY
119                     var options = pendingInfo.getDefaultSizeOptions(context)
120                     val isDirectConfig =
121                         item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)
122                     if (isDirectConfig && item.bindOptions != null) {
123                         val newOptions = item.bindOptions.extras
124                         if (options != null) {
125                             newOptions!!.putAll(options)
126                         }
127                         options = newOptions
128                     }
129                     val success =
130                         widgetHelper.bindAppWidgetIdIfAllowed(
131                             item.appWidgetId,
132                             appWidgetInfo,
133                             options,
134                         )
135 
136                     // We tried to bind once. If we were not able to bind, we would need to
137                     // go through the permission dialog, which means we cannot skip the config
138                     // activity.
139                     item.bindOptions = null
140                     item.restoreStatus =
141                         item.restoreStatus and LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG.inv()
142 
143                     // Bind succeeded
144                     if (success) {
145                         // If the widget has a configure activity, it is still needs to set it
146                         // up, otherwise the widget is ready to go.
147                         item.restoreStatus =
148                             if ((appWidgetInfo.configure == null) || isDirectConfig)
149                                 LauncherAppWidgetInfo.RESTORE_COMPLETED
150                             else LauncherAppWidgetInfo.FLAG_UI_NOT_READY
151                     }
152                     update = true
153                 }
154             } else if (
155                 (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) &&
156                     (appWidgetInfo.configure == null))
157             ) {
158                 // The widget was marked as UI not ready, but there is no configure activity to
159                 // update the UI.
160                 item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED
161                 update = true
162             } else if (
163                 (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) &&
164                     appWidgetInfo.configure != null)
165             ) {
166                 if (widgetHelper.isAppWidgetRestored(item.appWidgetId)) {
167                     item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED
168                     update = true
169                 }
170             }
171         }
172 
173         if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) {
174             // Verify that we own the widget
175             if (appWidgetInfo == null) {
176                 FileLog.e(Launcher.TAG, "Removing invalid widget: id=" + item.appWidgetId)
177                 return InflationResult(TYPE_DELETE, reason = removalReason)
178             }
179             item.minSpanX = appWidgetInfo.minSpanX
180             item.minSpanY = appWidgetInfo.minSpanY
181             return InflationResult(TYPE_REAL, isUpdate = update, widgetInfo = appWidgetInfo)
182         } else {
183             return InflationResult(TYPE_PENDING, isUpdate = update, widgetInfo = appWidgetInfo)
184         }
185     }
186 
187     data class InflationResult(
188         val type: Int,
189         val reason: String? = null,
190         @RestoreError
191         val restoreErrorType: String = RestoreError.UNSPECIFIED_WIDGET_INFLATION_RESULT,
192         val isUpdate: Boolean = false,
193         val widgetInfo: LauncherAppWidgetProviderInfo? = null,
194     )
195 
196     companion object {
197         const val TYPE_DELETE = 0
198 
199         const val TYPE_PENDING = 1
200 
201         const val TYPE_REAL = 2
202     }
203 }
204