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