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