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) return null; 279 Object tag = view.getTag(); 280 if (!(tag instanceof ItemInfo)) return null; 281 Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle(); 282 bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY); 283 return bundle; 284 } 285 286 /** 287 * Starts the binding flow for the widget 288 * @param activity The activity for which to bind the widget 289 * @param appWidgetId The ID of the widget 290 * @param info The {@link AppWidgetProviderInfo} of the widget 291 * @param requestCode The request code 292 */ startBindFlow(@onNull BaseActivity activity, int appWidgetId, @NonNull AppWidgetProviderInfo info, int requestCode)293 public void startBindFlow(@NonNull BaseActivity activity, 294 int appWidgetId, @NonNull AppWidgetProviderInfo info, int requestCode) { 295 if (WidgetsModel.GO_DISABLE_WIDGETS) { 296 sendActionCancelled(activity, requestCode); 297 return; 298 } 299 300 Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND) 301 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) 302 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider) 303 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile()); 304 // TODO: we need to make sure that this accounts for the options bundle. 305 // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options); 306 activity.startActivityForResult(intent, requestCode); 307 } 308 309 /** 310 * Stop the host from listening to the widget updates 311 */ stopListening()312 public void stopListening() { 313 if (WidgetsModel.GO_DISABLE_WIDGETS) { 314 return; 315 } 316 if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { 317 // Cache the content from the widgets when Launcher stops listening to widget updates 318 final LauncherAppState state = LauncherAppState.getInstance(mContext); 319 synchronized (state.mCachedRemoteViews) { 320 for (int i = 0; i < mViews.size(); i++) { 321 final int appWidgetId = mViews.keyAt(i); 322 final LauncherAppWidgetHostView view = mViews.get(appWidgetId); 323 state.mCachedRemoteViews.put(appWidgetId, view.mLastRemoteViews); 324 } 325 } 326 } 327 mWidgetHost.stopListening(); 328 setListeningFlag(false); 329 } 330 setListeningFlag(final boolean isListening)331 protected void setListeningFlag(final boolean isListening) { 332 if (isListening) { 333 mFlags |= FLAG_LISTENING; 334 return; 335 } 336 mFlags &= ~FLAG_LISTENING; 337 } 338 339 /** 340 * @return The app widget ids 341 */ 342 @NonNull getAppWidgetIds()343 public int[] getAppWidgetIds() { 344 return mWidgetHost.getAppWidgetIds(); 345 } 346 347 /** 348 * Create a view for the specified app widget 349 * @param context The activity context for which the view is created 350 * @param appWidgetId The ID of the widget 351 * @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget 352 * @return A view for the widget 353 */ 354 @NonNull createView(@onNull Context context, int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget)355 public AppWidgetHostView createView(@NonNull Context context, int appWidgetId, 356 @NonNull LauncherAppWidgetProviderInfo appWidget) { 357 if (appWidget.isCustomWidget()) { 358 LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context); 359 lahv.setAppWidget(0, appWidget); 360 CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv); 361 return lahv; 362 } else if ((mFlags & FLAG_LISTENING) == 0) { 363 // Since the launcher hasn't started listening to widget updates, we can't simply call 364 // super.createView here because the later will make a binder call to retrieve 365 // RemoteViews from system process. 366 // TODO: have launcher always listens to widget updates in background so that this 367 // check can be removed altogether. 368 if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { 369 final RemoteViews cachedRemoteViews = getCachedRemoteViews(appWidgetId); 370 if (cachedRemoteViews != null) { 371 // We've found RemoteViews from cache for this widget, so we will instantiate a 372 // widget host view and populate it with the cached RemoteViews. 373 final LauncherAppWidgetHostView view = new LauncherAppWidgetHostView(context); 374 view.setAppWidget(appWidgetId, appWidget); 375 view.updateAppWidget(cachedRemoteViews); 376 mDeferredViews.put(appWidgetId, view); 377 mViews.put(appWidgetId, view); 378 return view; 379 } 380 } 381 // If cache misses or not enabled, a placeholder for the widget will be returned. 382 DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context); 383 view.setAppWidget(appWidgetId, appWidget); 384 mViews.put(appWidgetId, view); 385 return view; 386 } else { 387 try { 388 return mWidgetHost.createView(context, appWidgetId, appWidget); 389 } catch (Exception e) { 390 if (!Utilities.isBinderSizeError(e)) { 391 throw new RuntimeException(e); 392 } 393 394 // If the exception was thrown while fetching the remote views, let the view stay. 395 // This will ensure that if the widget posts a valid update later, the view 396 // will update. 397 LauncherAppWidgetHostView view = mViews.get(appWidgetId); 398 if (view == null) { 399 view = onCreateView(mContext, appWidgetId, appWidget); 400 } 401 view.setAppWidget(appWidgetId, appWidget); 402 view.switchToErrorView(); 403 return view; 404 } 405 } 406 } 407 408 /** 409 * Listener for getting notifications on provider changes. 410 */ 411 public interface ProviderChangedListener { 412 /** 413 * Notify the listener that the providers have changed 414 */ notifyWidgetProvidersChanged()415 void notifyWidgetProvidersChanged(); 416 } 417 418 /** 419 * Called to return a proper view when creating a view 420 * @param context The context for which the widget view is created 421 * @param appWidgetId The ID of the added widget 422 * @param appWidget The provider info of the added widget 423 * @return A view for the specified app widget 424 */ 425 @NonNull onCreateView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget)426 public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId, 427 AppWidgetProviderInfo appWidget) { 428 final LauncherAppWidgetHostView view; 429 if (getPendingView(appWidgetId) != null) { 430 view = getPendingView(appWidgetId); 431 removePendingView(appWidgetId); 432 } else if (mDeferredViews.get(appWidgetId) != null) { 433 // In case the widget view is deferred, we will simply return the deferred view as 434 // opposed to instantiate a new instance of LauncherAppWidgetHostView since launcher 435 // already added the former to the workspace. 436 view = mDeferredViews.get(appWidgetId); 437 } else { 438 view = new LauncherAppWidgetHostView(context); 439 } 440 mViews.put(appWidgetId, view); 441 return view; 442 } 443 444 /** 445 * Clears all the views from the host 446 */ clearViews()447 public void clearViews() { 448 LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost; 449 tempHost.clearViews(); 450 if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) { 451 // Clear previously cached content from existing widgets 452 mDeferredViews.clear(); 453 } 454 mViews.clear(); 455 } 456 457 /** 458 * @return True if the host is listening to the updates, false otherwise 459 */ isListening()460 public boolean isListening() { 461 return (mFlags & FLAG_LISTENING) != 0; 462 } 463 464 /** 465 * Sets or unsets a flag the can change whether the widget host should be in the listening 466 * state. 467 */ setShouldListenFlag(int flag, boolean on)468 private void setShouldListenFlag(int flag, boolean on) { 469 if (on) { 470 mFlags |= flag; 471 } else { 472 mFlags &= ~flag; 473 } 474 475 final boolean listening = isListening(); 476 if (!listening && shouldListen(mFlags)) { 477 // Postpone starting listening until all flags are on. 478 startListening(); 479 } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) { 480 // Postpone stopping listening until the activity is stopped. 481 stopListening(); 482 } 483 } 484 485 /** 486 * Returns true if the holder should be listening for widget updates based 487 * on the provided state flags. 488 */ shouldListen(int flags)489 protected boolean shouldListen(int flags) { 490 return (flags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN; 491 } 492 493 @Nullable getCachedRemoteViews(int appWidgetId)494 private RemoteViews getCachedRemoteViews(int appWidgetId) { 495 final LauncherAppState state = LauncherAppState.getInstance(mContext); 496 synchronized (state.mCachedRemoteViews) { 497 return state.mCachedRemoteViews.get(appWidgetId); 498 } 499 } 500 501 /** 502 * Returns the new LauncherWidgetHolder instance 503 */ newInstance(Context context)504 public static LauncherWidgetHolder newInstance(Context context) { 505 return HolderFactory.newFactory(context).newInstance(context, null); 506 } 507 508 /** 509 * A factory class that generates new instances of {@code LauncherWidgetHolder} 510 */ 511 public static class HolderFactory implements ResourceBasedOverride { 512 513 /** 514 * @param context The context of the caller 515 * @param appWidgetRemovedCallback The callback that is called when widgets are removed 516 * @return A new instance of {@code LauncherWidgetHolder} 517 */ newInstance(@onNull Context context, @Nullable IntConsumer appWidgetRemovedCallback)518 public LauncherWidgetHolder newInstance(@NonNull Context context, 519 @Nullable IntConsumer appWidgetRemovedCallback) { 520 return new LauncherWidgetHolder(context, appWidgetRemovedCallback); 521 } 522 523 /** 524 * @param context The context of the caller 525 * @return A new instance of factory class for widget holders. If not specified, returning 526 * {@code HolderFactory} by default. 527 */ newFactory(Context context)528 public static HolderFactory newFactory(Context context) { 529 return Overrides.getObject( 530 HolderFactory.class, context, R.string.widget_holder_factory_class); 531 } 532 } 533 } 534