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