1 /** 2 * Copyright (C) 2022 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 package com.android.launcher3.widget; 17 18 import static android.app.Activity.RESULT_CANCELED; 19 20 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED; 21 import static com.android.launcher3.Flags.enableWorkspaceInflation; 22 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 23 import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo; 24 25 import android.appwidget.AppWidgetHost; 26 import android.appwidget.AppWidgetHostView; 27 import android.appwidget.AppWidgetManager; 28 import android.appwidget.AppWidgetProviderInfo; 29 import android.content.ActivityNotFoundException; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.os.Bundle; 33 import android.os.Looper; 34 import android.util.SparseArray; 35 import android.widget.Toast; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 40 import com.android.launcher3.BaseActivity; 41 import com.android.launcher3.BaseDraggingActivity; 42 import com.android.launcher3.R; 43 import com.android.launcher3.Utilities; 44 import com.android.launcher3.model.data.ItemInfo; 45 import com.android.launcher3.testing.TestLogging; 46 import com.android.launcher3.testing.shared.TestProtocol; 47 import com.android.launcher3.util.ResourceBasedOverride; 48 import com.android.launcher3.util.SafeCloseable; 49 import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView; 50 import com.android.launcher3.widget.custom.CustomWidgetManager; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 import java.util.function.IntConsumer; 55 56 /** 57 * A wrapper for LauncherAppWidgetHost. This class is created so the AppWidgetHost could run in 58 * background. 59 */ 60 public class LauncherWidgetHolder { 61 public static final int APPWIDGET_HOST_ID = 1024; 62 63 protected static final int FLAG_LISTENING = 1; 64 protected static final int FLAG_STATE_IS_NORMAL = 1 << 1; 65 protected static final int FLAG_ACTIVITY_STARTED = 1 << 2; 66 protected static final int FLAG_ACTIVITY_RESUMED = 1 << 3; 67 private static final int FLAGS_SHOULD_LISTEN = 68 FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED; 69 70 @NonNull 71 protected final Context mContext; 72 73 @NonNull 74 private final AppWidgetHost mWidgetHost; 75 76 @NonNull 77 protected final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>(); 78 protected final List<ProviderChangedListener> mProviderChangedListeners = new ArrayList<>(); 79 80 protected int mFlags = FLAG_STATE_IS_NORMAL; 81 82 // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden 83 private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle"; 84 // TODO(b/191735836): Replace with SplashScreen.SPLASH_SCREEN_STYLE_EMPTY when un-hidden 85 private static final int SPLASH_SCREEN_STYLE_EMPTY = 0; 86 LauncherWidgetHolder(@onNull Context context, @Nullable IntConsumer appWidgetRemovedCallback)87 protected LauncherWidgetHolder(@NonNull Context context, 88 @Nullable IntConsumer appWidgetRemovedCallback) { 89 mContext = context; 90 mWidgetHost = createHost(context, appWidgetRemovedCallback); 91 } 92 createHost( Context context, @Nullable IntConsumer appWidgetRemovedCallback)93 protected AppWidgetHost createHost( 94 Context context, @Nullable IntConsumer appWidgetRemovedCallback) { 95 return new LauncherAppWidgetHost( 96 context, appWidgetRemovedCallback, mProviderChangedListeners); 97 } 98 99 /** 100 * Starts listening to the widget updates from the server side 101 */ startListening()102 public void startListening() { 103 if (!WIDGETS_ENABLED) { 104 return; 105 } 106 107 try { 108 mWidgetHost.startListening(); 109 } catch (Exception e) { 110 if (!Utilities.isBinderSizeError(e)) { 111 throw new RuntimeException(e); 112 } 113 // We're willing to let this slide. The exception is being caused by the list of 114 // RemoteViews which is being passed back. The startListening relationship will 115 // have been established by this point, and we will end up populating the 116 // widgets upon bind anyway. See issue 14255011 for more context. 117 } 118 // TODO: Investigate why widgetHost.startListening() always return non-empty updates 119 setListeningFlag(true); 120 121 updateDeferredView(); 122 } 123 124 /** 125 * Update any views which have been deferred because the host was not listening. 126 */ updateDeferredView()127 protected void updateDeferredView() { 128 // Update any views which have been deferred because the host was not listening. 129 // We go in reverse order and inflate any deferred or cached widget 130 for (int i = mViews.size() - 1; i >= 0; i--) { 131 LauncherAppWidgetHostView view = mViews.valueAt(i); 132 if (view instanceof PendingAppWidgetHostView pv) { 133 pv.reInflate(); 134 } 135 } 136 } 137 138 /** 139 * Registers an "activity started/stopped" event. 140 */ setActivityStarted(boolean isStarted)141 public void setActivityStarted(boolean isStarted) { 142 setShouldListenFlag(FLAG_ACTIVITY_STARTED, isStarted); 143 } 144 145 /** 146 * Registers an "activity paused/resumed" event. 147 */ setActivityResumed(boolean isResumed)148 public void setActivityResumed(boolean isResumed) { 149 setShouldListenFlag(FLAG_ACTIVITY_RESUMED, isResumed); 150 } 151 152 /** 153 * Set the NORMAL state of the widget host 154 * @param isNormal True if setting the host to be in normal state, false otherwise 155 */ setStateIsNormal(boolean isNormal)156 public void setStateIsNormal(boolean isNormal) { 157 setShouldListenFlag(FLAG_STATE_IS_NORMAL, isNormal); 158 } 159 160 /** 161 * Delete the specified app widget from the host 162 * @param appWidgetId The ID of the app widget to be deleted 163 */ deleteAppWidgetId(int appWidgetId)164 public void deleteAppWidgetId(int appWidgetId) { 165 mWidgetHost.deleteAppWidgetId(appWidgetId); 166 mViews.remove(appWidgetId); 167 } 168 169 /** 170 * Called when the launcher is destroyed 171 */ destroy()172 public void destroy() { 173 // No-op 174 } 175 176 /** 177 * @return The allocated app widget id if allocation is successful, returns -1 otherwise 178 */ allocateAppWidgetId()179 public int allocateAppWidgetId() { 180 if (!WIDGETS_ENABLED) { 181 return AppWidgetManager.INVALID_APPWIDGET_ID; 182 } 183 184 return mWidgetHost.allocateAppWidgetId(); 185 } 186 187 /** 188 * Add a listener that is triggered when the providers of the widgets are changed 189 * @param listener The listener that notifies when the providers changed 190 */ addProviderChangeListener( @onNull LauncherWidgetHolder.ProviderChangedListener listener)191 public void addProviderChangeListener( 192 @NonNull LauncherWidgetHolder.ProviderChangedListener listener) { 193 MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.add(listener)); 194 } 195 196 /** 197 * Remove the specified listener from the host 198 * @param listener The listener that is to be removed from the host 199 */ removeProviderChangeListener( LauncherWidgetHolder.ProviderChangedListener listener)200 public void removeProviderChangeListener( 201 LauncherWidgetHolder.ProviderChangedListener listener) { 202 MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.remove(listener)); 203 } 204 205 /** 206 * Starts the configuration activity for the widget 207 * @param activity The activity in which to start the configuration page 208 * @param widgetId The ID of the widget 209 * @param requestCode The request code 210 */ startConfigActivity(@onNull BaseDraggingActivity activity, int widgetId, int requestCode)211 public void startConfigActivity(@NonNull BaseDraggingActivity activity, int widgetId, 212 int requestCode) { 213 if (!WIDGETS_ENABLED) { 214 sendActionCancelled(activity, requestCode); 215 return; 216 } 217 218 try { 219 TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startConfigActivity"); 220 mWidgetHost.startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode, 221 getConfigurationActivityOptions(activity, widgetId)); 222 } catch (ActivityNotFoundException | SecurityException e) { 223 Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); 224 sendActionCancelled(activity, requestCode); 225 } 226 } 227 sendActionCancelled(final BaseActivity activity, final int requestCode)228 private void sendActionCancelled(final BaseActivity activity, final int requestCode) { 229 MAIN_EXECUTOR.execute( 230 () -> activity.onActivityResult(requestCode, RESULT_CANCELED, null)); 231 } 232 233 /** 234 * Returns an {@link android.app.ActivityOptions} bundle from the {code activity} for launching 235 * the configuration of the {@code widgetId} app widget, or null of options cannot be produced. 236 */ 237 @Nullable getConfigurationActivityOptions(@onNull BaseDraggingActivity activity, int widgetId)238 protected Bundle getConfigurationActivityOptions(@NonNull BaseDraggingActivity activity, 239 int widgetId) { 240 LauncherAppWidgetHostView view = mViews.get(widgetId); 241 if (view == null) { 242 return activity.makeDefaultActivityOptions( 243 -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */).toBundle(); 244 } 245 Object tag = view.getTag(); 246 if (!(tag instanceof ItemInfo)) { 247 return activity.makeDefaultActivityOptions( 248 -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */).toBundle(); 249 } 250 Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle(); 251 bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY); 252 return bundle; 253 } 254 255 /** 256 * Starts the binding flow for the widget 257 * @param activity The activity for which to bind the widget 258 * @param appWidgetId The ID of the widget 259 * @param info The {@link AppWidgetProviderInfo} of the widget 260 * @param requestCode The request code 261 */ startBindFlow(@onNull BaseActivity activity, int appWidgetId, @NonNull AppWidgetProviderInfo info, int requestCode)262 public void startBindFlow(@NonNull BaseActivity activity, 263 int appWidgetId, @NonNull AppWidgetProviderInfo info, int requestCode) { 264 if (!WIDGETS_ENABLED) { 265 sendActionCancelled(activity, requestCode); 266 return; 267 } 268 269 Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND) 270 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) 271 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider) 272 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile()); 273 // TODO: we need to make sure that this accounts for the options bundle. 274 // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options); 275 activity.startActivityForResult(intent, requestCode); 276 } 277 278 /** 279 * Stop the host from listening to the widget updates 280 */ stopListening()281 public void stopListening() { 282 if (!WIDGETS_ENABLED) { 283 return; 284 } 285 mWidgetHost.stopListening(); 286 setListeningFlag(false); 287 } 288 setListeningFlag(final boolean isListening)289 protected void setListeningFlag(final boolean isListening) { 290 if (isListening) { 291 mFlags |= FLAG_LISTENING; 292 return; 293 } 294 mFlags &= ~FLAG_LISTENING; 295 } 296 297 /** 298 * @return The app widget ids 299 */ 300 @NonNull getAppWidgetIds()301 public int[] getAppWidgetIds() { 302 return mWidgetHost.getAppWidgetIds(); 303 } 304 305 /** 306 * Adds a callback to be run everytime the provided app widget updates. 307 * @return a closable to remove this callback 308 */ addOnUpdateListener( int appWidgetId, LauncherAppWidgetProviderInfo appWidget, Runnable callback)309 public SafeCloseable addOnUpdateListener( 310 int appWidgetId, LauncherAppWidgetProviderInfo appWidget, Runnable callback) { 311 if (createView(appWidgetId, appWidget) instanceof ListenableHostView lhv) { 312 return lhv.addUpdateListener(callback); 313 } 314 return () -> { }; 315 } 316 317 /** 318 * Create a view for the specified app widget. When calling this method from a background 319 * thread, the returned view will not receive ongoing updates. The caller needs to reattach 320 * the view using {@link #attachViewToHostAndGetAttachedView} on UIThread 321 * 322 * @param appWidgetId The ID of the widget 323 * @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget 324 * @return A view for the widget 325 */ 326 @NonNull createView( int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget)327 public AppWidgetHostView createView( 328 int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { 329 if (appWidget.isCustomWidget()) { 330 LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(mContext); 331 lahv.setAppWidget(0, appWidget); 332 CustomWidgetManager.INSTANCE.get(mContext).onViewCreated(lahv); 333 return lahv; 334 } 335 336 LauncherAppWidgetHostView view = createViewInternal(appWidgetId, appWidget); 337 // Do not update mViews on a background thread call, as the holder is not thread safe. 338 if (!enableWorkspaceInflation() || Looper.myLooper() == Looper.getMainLooper()) { 339 mViews.put(appWidgetId, view); 340 } 341 return view; 342 } 343 344 /** 345 * Attaches an already inflated view to the host. If the view can't be attached, creates 346 * and attaches a new view. 347 * @return the final attached view 348 */ 349 @NonNull attachViewToHostAndGetAttachedView( @onNull LauncherAppWidgetHostView view)350 public final AppWidgetHostView attachViewToHostAndGetAttachedView( 351 @NonNull LauncherAppWidgetHostView view) { 352 353 // Binder can also inflate placeholder widgets in case of backup-restore. Skip 354 // attaching such widgets 355 boolean isRealWidget = ((view instanceof PendingAppWidgetHostView pw) 356 ? pw.isDeferredWidget() : true) 357 && view.getAppWidgetInfo() != null; 358 if (isRealWidget && mViews.get(view.getAppWidgetId()) != view) { 359 view = recycleExistingView(view); 360 mViews.put(view.getAppWidgetId(), view); 361 } 362 return view; 363 } 364 365 /** 366 * Recycling logic: 367 * 1) If the final view should be a pendingView 368 * if the provided view is also a pendingView, return itself 369 * otherwise discard provided view and return a new pending view 370 * 2) If the recycled view is a pendingView, discard it and return a new view 371 * 3) Use the same for as creating a new view, but used the provided view in the host instead 372 * of creating a new view. This ensures that all the host callbacks are properly attached 373 * as a result of using the same flow. 374 */ recycleExistingView(LauncherAppWidgetHostView view)375 protected LauncherAppWidgetHostView recycleExistingView(LauncherAppWidgetHostView view) { 376 if ((mFlags & FLAG_LISTENING) == 0) { 377 if (view instanceof PendingAppWidgetHostView pv && pv.isDeferredWidget()) { 378 return view; 379 } else { 380 return new PendingAppWidgetHostView(mContext, this, view.getAppWidgetId(), 381 fromProviderInfo(mContext, view.getAppWidgetInfo())); 382 } 383 } 384 LauncherAppWidgetHost host = (LauncherAppWidgetHost) mWidgetHost; 385 if (view instanceof ListenableHostView lhv) { 386 host.recycleViewForNextCreation(lhv); 387 } 388 389 view = createViewInternal( 390 view.getAppWidgetId(), fromProviderInfo(mContext, view.getAppWidgetInfo())); 391 host.recycleViewForNextCreation(null); 392 return view; 393 } 394 395 @NonNull createViewInternal( int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget)396 protected LauncherAppWidgetHostView createViewInternal( 397 int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { 398 if ((mFlags & FLAG_LISTENING) == 0) { 399 // Since the launcher hasn't started listening to widget updates, we can't simply call 400 // host.createView here because the later will make a binder call to retrieve 401 // RemoteViews from system process. 402 return new PendingAppWidgetHostView(mContext, this, appWidgetId, appWidget); 403 } else { 404 if (enableWorkspaceInflation() && Looper.myLooper() != Looper.getMainLooper()) { 405 // Widget is being inflated a background thread, just create and 406 // return a placeholder view 407 ListenableHostView hostView = new ListenableHostView(mContext); 408 hostView.setAppWidget(appWidgetId, appWidget); 409 return hostView; 410 } 411 try { 412 return (LauncherAppWidgetHostView) mWidgetHost.createView( 413 mContext, appWidgetId, appWidget); 414 } catch (Exception e) { 415 if (!Utilities.isBinderSizeError(e)) { 416 throw new RuntimeException(e); 417 } 418 419 // If the exception was thrown while fetching the remote views, let the view stay. 420 // This will ensure that if the widget posts a valid update later, the view 421 // will update. 422 LauncherAppWidgetHostView view = mViews.get(appWidgetId); 423 if (view == null) { 424 view = new ListenableHostView(mContext); 425 } 426 view.setAppWidget(appWidgetId, appWidget); 427 view.switchToErrorView(); 428 return view; 429 } 430 } 431 } 432 433 /** 434 * Listener for getting notifications on provider changes. 435 */ 436 public interface ProviderChangedListener { 437 /** 438 * Notify the listener that the providers have changed 439 */ notifyWidgetProvidersChanged()440 void notifyWidgetProvidersChanged(); 441 } 442 443 /** 444 * Clears all the views from the host 445 */ clearViews()446 public void clearViews() { 447 LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost; 448 tempHost.clearViews(); 449 mViews.clear(); 450 } 451 452 /** 453 * Clears all the internal widget views 454 */ clearWidgetViews()455 public void clearWidgetViews() { 456 clearViews(); 457 } 458 459 /** 460 * @return True if the host is listening to the updates, false otherwise 461 */ isListening()462 public boolean isListening() { 463 return (mFlags & FLAG_LISTENING) != 0; 464 } 465 466 /** 467 * Sets or unsets a flag the can change whether the widget host should be in the listening 468 * state. 469 */ setShouldListenFlag(int flag, boolean on)470 private void setShouldListenFlag(int flag, boolean on) { 471 if (on) { 472 mFlags |= flag; 473 } else { 474 mFlags &= ~flag; 475 } 476 477 final boolean listening = isListening(); 478 if (!listening && shouldListen(mFlags)) { 479 // Postpone starting listening until all flags are on. 480 startListening(); 481 } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) { 482 // Postpone stopping listening until the activity is stopped. 483 stopListening(); 484 } 485 } 486 487 /** 488 * Returns true if the holder should be listening for widget updates based 489 * on the provided state flags. 490 */ shouldListen(int flags)491 protected boolean shouldListen(int flags) { 492 return (flags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN; 493 } 494 495 /** 496 * Returns the new LauncherWidgetHolder instance 497 */ newInstance(Context context)498 public static LauncherWidgetHolder newInstance(Context context) { 499 return HolderFactory.newFactory(context).newInstance(context, null); 500 } 501 502 /** 503 * A factory class that generates new instances of {@code LauncherWidgetHolder} 504 */ 505 public static class HolderFactory implements ResourceBasedOverride { 506 507 /** 508 * @param context The context of the caller 509 * @param appWidgetRemovedCallback The callback that is called when widgets are removed 510 * @return A new instance of {@code LauncherWidgetHolder} 511 */ newInstance(@onNull Context context, @Nullable IntConsumer appWidgetRemovedCallback)512 public LauncherWidgetHolder newInstance(@NonNull Context context, 513 @Nullable IntConsumer appWidgetRemovedCallback) { 514 return new LauncherWidgetHolder(context, appWidgetRemovedCallback); 515 } 516 517 /** 518 * @param context The context of the caller 519 * @return A new instance of factory class for widget holders. If not specified, returning 520 * {@code HolderFactory} by default. 521 */ newFactory(Context context)522 public static HolderFactory newFactory(Context context) { 523 return Overrides.getObject( 524 HolderFactory.class, context, R.string.widget_holder_factory_class); 525 } 526 } 527 } 528