• 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.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