• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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