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