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