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 com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
20 
21 import android.appwidget.AppWidgetHost;
22 import android.appwidget.AppWidgetProviderInfo;
23 import android.content.Context;
24 import android.view.accessibility.AccessibilityNodeInfo;
25 import android.widget.RemoteViews;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import com.android.launcher3.LauncherAppState;
31 import com.android.launcher3.util.Executors;
32 import com.android.launcher3.util.SafeCloseable;
33 import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
34 
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
38 import java.util.Set;
39 import java.util.WeakHashMap;
40 import java.util.function.IntConsumer;
41 
42 /**
43  * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView}
44  * which correctly captures all long-press events. This ensures that users can
45  * always pick up and move widgets.
46  */
47 class LauncherAppWidgetHost extends AppWidgetHost {
48     @NonNull
49     private final List<ProviderChangedListener> mProviderChangeListeners;
50 
51     @NonNull
52     private final Context mContext;
53 
54     @Nullable
55     private final IntConsumer mAppWidgetRemovedCallback;
56 
57     @Nullable
58     private ListenableHostView mViewToRecycle;
59 
LauncherAppWidgetHost(@onNull Context context, @Nullable IntConsumer appWidgetRemovedCallback, List<ProviderChangedListener> providerChangeListeners)60     public LauncherAppWidgetHost(@NonNull Context context,
61             @Nullable IntConsumer appWidgetRemovedCallback,
62             List<ProviderChangedListener> providerChangeListeners) {
63         super(context, APPWIDGET_HOST_ID);
64         mContext = context;
65         mAppWidgetRemovedCallback = appWidgetRemovedCallback;
66         mProviderChangeListeners = providerChangeListeners;
67     }
68 
69     @Override
onProvidersChanged()70     protected void onProvidersChanged() {
71         if (!mProviderChangeListeners.isEmpty()) {
72             for (LauncherWidgetHolder.ProviderChangedListener callback :
73                     new ArrayList<>(mProviderChangeListeners)) {
74                 callback.notifyWidgetProvidersChanged();
75             }
76         }
77     }
78 
79     /**
80      * Sets the view to be recycled for the next widget creation.
81      */
recycleViewForNextCreation(ListenableHostView viewToRecycle)82     public void recycleViewForNextCreation(ListenableHostView viewToRecycle) {
83         mViewToRecycle = viewToRecycle;
84     }
85 
86     @Override
87     @NonNull
onCreateView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget)88     public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
89             AppWidgetProviderInfo appWidget) {
90         ListenableHostView result =
91                 mViewToRecycle != null ? mViewToRecycle : new ListenableHostView(context);
92         mViewToRecycle = null;
93         return result;
94     }
95 
96     /**
97      * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk.
98      */
99     @Override
onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget)100     protected void onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget) {
101         LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
102                 mContext, appWidget);
103         super.onProviderChanged(appWidgetId, info);
104         // The super method updates the dimensions of the providerInfo. Update the
105         // launcher spans accordingly.
106         info.initSpans(mContext, LauncherAppState.getIDP(mContext));
107     }
108 
109     /**
110      * Called on an appWidget is removed for a widgetId
111      *
112      * @param appWidgetId TODO: make this override when SDK is updated
113      */
114     @Override
onAppWidgetRemoved(int appWidgetId)115     public void onAppWidgetRemoved(int appWidgetId) {
116         if (mAppWidgetRemovedCallback == null) {
117             return;
118         }
119         // Route the call via model thread, in case it comes while a loader-bind is in progress
120         Executors.MODEL_EXECUTOR.execute(
121                 () -> Executors.MAIN_EXECUTOR.execute(
122                         () -> mAppWidgetRemovedCallback.accept(appWidgetId)));
123     }
124 
125     /**
126      * The same as super.clearViews(), except with the scope exposed
127      */
128     @Override
clearViews()129     public void clearViews() {
130         super.clearViews();
131     }
132 
133     public static class ListenableHostView extends LauncherAppWidgetHostView {
134 
135         private Set<Runnable> mUpdateListeners = Collections.EMPTY_SET;
136 
ListenableHostView(Context context)137         ListenableHostView(Context context) {
138             super(context);
139         }
140 
141         @Override
updateAppWidget(RemoteViews remoteViews)142         public void updateAppWidget(RemoteViews remoteViews) {
143             super.updateAppWidget(remoteViews);
144             mUpdateListeners.forEach(Runnable::run);
145         }
146 
147         @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)148         public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
149             super.onInitializeAccessibilityNodeInfo(info);
150             info.setClassName(LauncherAppWidgetHostView.class.getName());
151         }
152 
153         /**
154          * Adds a callback to be run everytime the provided app widget updates.
155          * @return a closable to remove this callback
156          */
addUpdateListener(Runnable callback)157         public SafeCloseable addUpdateListener(Runnable callback) {
158             if (mUpdateListeners == Collections.EMPTY_SET) {
159                 mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>());
160             }
161             mUpdateListeners.add(callback);
162             return () -> mUpdateListeners.remove(callback);
163         }
164     }
165 }
166