• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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 
17 package com.android.launcher3.widget;
18 
19 import static android.app.Activity.RESULT_CANCELED;
20 
21 import android.appwidget.AppWidgetHost;
22 import android.appwidget.AppWidgetHostView;
23 import android.appwidget.AppWidgetManager;
24 import android.appwidget.AppWidgetProviderInfo;
25 import android.content.ActivityNotFoundException;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.util.SparseArray;
31 import android.widget.Toast;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.android.launcher3.BaseActivity;
36 import com.android.launcher3.BaseDraggingActivity;
37 import com.android.launcher3.LauncherAppState;
38 import com.android.launcher3.R;
39 import com.android.launcher3.Utilities;
40 import com.android.launcher3.model.WidgetsModel;
41 import com.android.launcher3.model.data.ItemInfo;
42 import com.android.launcher3.testing.TestLogging;
43 import com.android.launcher3.testing.TestProtocol;
44 import com.android.launcher3.widget.custom.CustomWidgetManager;
45 
46 import java.util.ArrayList;
47 import java.util.function.IntConsumer;
48 
49 
50 /**
51  * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView}
52  * which correctly captures all long-press events. This ensures that users can
53  * always pick up and move widgets.
54  */
55 public class LauncherAppWidgetHost extends AppWidgetHost {
56 
57     private static final int FLAG_LISTENING = 1;
58     private static final int FLAG_STATE_IS_NORMAL = 1 << 1;
59     private static final int FLAG_ACTIVITY_STARTED = 1 << 2;
60     private static final int FLAG_ACTIVITY_RESUMED = 1 << 3;
61     private static final int FLAGS_SHOULD_LISTEN =
62             FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED;
63     // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden
64     private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle";
65     // TODO(b/191735836): Replace with SplashScreen.SPLASH_SCREEN_STYLE_EMPTY when un-hidden
66     private static final int SPLASH_SCREEN_STYLE_EMPTY = 0;
67 
68     public static final int APPWIDGET_HOST_ID = 1024;
69 
70     private final ArrayList<ProviderChangedListener> mProviderChangeListeners = new ArrayList<>();
71     private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
72     private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
73 
74     private final Context mContext;
75     private int mFlags = FLAG_STATE_IS_NORMAL;
76 
77     private IntConsumer mAppWidgetRemovedCallback = null;
78 
79 
LauncherAppWidgetHost(Context context)80     public LauncherAppWidgetHost(Context context) {
81         this(context, null);
82     }
83 
LauncherAppWidgetHost(Context context, IntConsumer appWidgetRemovedCallback)84     public LauncherAppWidgetHost(Context context,
85             IntConsumer appWidgetRemovedCallback) {
86         super(context, APPWIDGET_HOST_ID);
87         mContext = context;
88         mAppWidgetRemovedCallback = appWidgetRemovedCallback;
89     }
90 
91     @Override
onCreateView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget)92     protected LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
93             AppWidgetProviderInfo appWidget) {
94         final LauncherAppWidgetHostView view;
95         if (mPendingViews.get(appWidgetId) != null) {
96             view = mPendingViews.get(appWidgetId);
97             mPendingViews.remove(appWidgetId);
98         } else {
99             view = new LauncherAppWidgetHostView(context);
100         }
101         mViews.put(appWidgetId, view);
102         return view;
103     }
104 
105     @Override
startListening()106     public void startListening() {
107         if (WidgetsModel.GO_DISABLE_WIDGETS) {
108             return;
109         }
110         mFlags |= FLAG_LISTENING;
111         try {
112             super.startListening();
113         } catch (Exception e) {
114             if (!Utilities.isBinderSizeError(e)) {
115                 throw new RuntimeException(e);
116             }
117             // We're willing to let this slide. The exception is being caused by the list of
118             // RemoteViews which is being passed back. The startListening relationship will
119             // have been established by this point, and we will end up populating the
120             // widgets upon bind anyway. See issue 14255011 for more context.
121         }
122 
123         // We go in reverse order and inflate any deferred widget
124         for (int i = mViews.size() - 1; i >= 0; i--) {
125             LauncherAppWidgetHostView view = mViews.valueAt(i);
126             if (view instanceof DeferredAppWidgetHostView) {
127                 view.reInflate();
128             }
129         }
130     }
131 
132     @Override
stopListening()133     public void stopListening() {
134         if (WidgetsModel.GO_DISABLE_WIDGETS) {
135             return;
136         }
137         mFlags &= ~FLAG_LISTENING;
138         super.stopListening();
139     }
140 
isListening()141     public boolean isListening() {
142         return (mFlags & FLAG_LISTENING) != 0;
143     }
144 
145     /**
146      * Sets or unsets a flag the can change whether the widget host should be in the listening
147      * state.
148      */
setShouldListenFlag(int flag, boolean on)149     private void setShouldListenFlag(int flag, boolean on) {
150         if (on) {
151             mFlags |= flag;
152         } else {
153             mFlags &= ~flag;
154         }
155 
156         final boolean listening = isListening();
157         if (!listening && (mFlags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN) {
158             // Postpone starting listening until all flags are on.
159             startListening();
160         } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) {
161             // Postpone stopping listening until the activity is stopped.
162             stopListening();
163         }
164     }
165 
166     /**
167      * Registers an "entering/leaving Normal state" event.
168      */
setStateIsNormal(boolean isNormal)169     public void setStateIsNormal(boolean isNormal) {
170         setShouldListenFlag(FLAG_STATE_IS_NORMAL, isNormal);
171     }
172 
173     /**
174      * Registers an "activity started/stopped" event.
175      */
setActivityStarted(boolean isStarted)176     public void setActivityStarted(boolean isStarted) {
177         setShouldListenFlag(FLAG_ACTIVITY_STARTED, isStarted);
178     }
179 
180     /**
181      * Registers an "activity paused/resumed" event.
182      */
setActivityResumed(boolean isResumed)183     public void setActivityResumed(boolean isResumed) {
184         setShouldListenFlag(FLAG_ACTIVITY_RESUMED, isResumed);
185     }
186 
187     @Override
allocateAppWidgetId()188     public int allocateAppWidgetId() {
189         if (WidgetsModel.GO_DISABLE_WIDGETS) {
190             return AppWidgetManager.INVALID_APPWIDGET_ID;
191         }
192 
193         return super.allocateAppWidgetId();
194     }
195 
addProviderChangeListener(ProviderChangedListener callback)196     public void addProviderChangeListener(ProviderChangedListener callback) {
197         mProviderChangeListeners.add(callback);
198     }
199 
removeProviderChangeListener(ProviderChangedListener callback)200     public void removeProviderChangeListener(ProviderChangedListener callback) {
201         mProviderChangeListeners.remove(callback);
202     }
203 
onProvidersChanged()204     protected void onProvidersChanged() {
205         if (!mProviderChangeListeners.isEmpty()) {
206             for (ProviderChangedListener callback : new ArrayList<>(mProviderChangeListeners)) {
207                 callback.notifyWidgetProvidersChanged();
208             }
209         }
210     }
211 
addPendingView(int appWidgetId, PendingAppWidgetHostView view)212     public void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
213         mPendingViews.put(appWidgetId, view);
214     }
215 
createView(Context context, int appWidgetId, LauncherAppWidgetProviderInfo appWidget)216     public AppWidgetHostView createView(Context context, int appWidgetId,
217             LauncherAppWidgetProviderInfo appWidget) {
218         if (appWidget.isCustomWidget()) {
219             LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
220             lahv.setAppWidget(0, appWidget);
221             CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv);
222             return lahv;
223         } else if ((mFlags & FLAG_LISTENING) == 0) {
224             DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context);
225             view.setAppWidget(appWidgetId, appWidget);
226             mViews.put(appWidgetId, view);
227             return view;
228         } else {
229             try {
230                 return super.createView(context, appWidgetId, appWidget);
231             } catch (Exception e) {
232                 if (!Utilities.isBinderSizeError(e)) {
233                     throw new RuntimeException(e);
234                 }
235 
236                 // If the exception was thrown while fetching the remote views, let the view stay.
237                 // This will ensure that if the widget posts a valid update later, the view
238                 // will update.
239                 LauncherAppWidgetHostView view = mViews.get(appWidgetId);
240                 if (view == null) {
241                     view = onCreateView(mContext, appWidgetId, appWidget);
242                 }
243                 view.setAppWidget(appWidgetId, appWidget);
244                 view.switchToErrorView();
245                 return view;
246             }
247         }
248     }
249 
250     /**
251      * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk.
252      */
253     @Override
onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget)254     protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) {
255         LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
256                 mContext, appWidget);
257         super.onProviderChanged(appWidgetId, info);
258         // The super method updates the dimensions of the providerInfo. Update the
259         // launcher spans accordingly.
260         info.initSpans(mContext, LauncherAppState.getIDP(mContext));
261     }
262 
263     /**
264      * Called on an appWidget is removed for a widgetId
265      *
266      * @param appWidgetId TODO: make this override when SDK is updated
267      */
onAppWidgetRemoved(int appWidgetId)268     public void onAppWidgetRemoved(int appWidgetId) {
269         if (mAppWidgetRemovedCallback == null) {
270             return;
271         }
272         mAppWidgetRemovedCallback.accept(appWidgetId);
273     }
274 
275     @Override
deleteAppWidgetId(int appWidgetId)276     public void deleteAppWidgetId(int appWidgetId) {
277         super.deleteAppWidgetId(appWidgetId);
278         mViews.remove(appWidgetId);
279     }
280 
281     @Override
clearViews()282     public void clearViews() {
283         super.clearViews();
284         mViews.clear();
285     }
286 
startBindFlow(BaseActivity activity, int appWidgetId, AppWidgetProviderInfo info, int requestCode)287     public void startBindFlow(BaseActivity activity,
288             int appWidgetId, AppWidgetProviderInfo info, int requestCode) {
289 
290         if (WidgetsModel.GO_DISABLE_WIDGETS) {
291             sendActionCancelled(activity, requestCode);
292             return;
293         }
294 
295         Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
296                 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
297                 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
298                 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile());
299         // TODO: we need to make sure that this accounts for the options bundle.
300         // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
301         activity.startActivityForResult(intent, requestCode);
302     }
303 
304     /**
305      * Launches an app widget's configuration activity.
306      * @param activity The activity from which to launch the configuration activity
307      * @param widgetId The id of the bound app widget to be configured
308      * @param requestCode An optional request code to be returned with the result
309      */
startConfigActivity(BaseDraggingActivity activity, int widgetId, int requestCode)310     public void startConfigActivity(BaseDraggingActivity activity, int widgetId, int requestCode) {
311         if (WidgetsModel.GO_DISABLE_WIDGETS) {
312             sendActionCancelled(activity, requestCode);
313             return;
314         }
315 
316         try {
317             TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startConfigActivity");
318             startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode,
319                     getConfigurationActivityOptions(activity, widgetId));
320         } catch (ActivityNotFoundException | SecurityException e) {
321             Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
322             sendActionCancelled(activity, requestCode);
323         }
324     }
325 
326     /**
327      * Returns an {@link android.app.ActivityOptions} bundle from the {code activity} for launching
328      * the configuration of the {@code widgetId} app widget, or null of options cannot be produced.
329      */
330     @Nullable
getConfigurationActivityOptions(BaseDraggingActivity activity, int widgetId)331     private Bundle getConfigurationActivityOptions(BaseDraggingActivity activity, int widgetId) {
332         LauncherAppWidgetHostView view = mViews.get(widgetId);
333         if (view == null) return null;
334         Object tag = view.getTag();
335         if (!(tag instanceof ItemInfo)) return null;
336         Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle();
337         bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY);
338         return bundle;
339     }
340 
sendActionCancelled(final BaseActivity activity, final int requestCode)341     private void sendActionCancelled(final BaseActivity activity, final int requestCode) {
342         new Handler().post(() -> activity.onActivityResult(requestCode, RESULT_CANCELED, null));
343     }
344 
345     /**
346      * Listener for getting notifications on provider changes.
347      */
348     public interface ProviderChangedListener {
349 
notifyWidgetProvidersChanged()350         void notifyWidgetProvidersChanged();
351     }
352 }
353