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
17package com.android.launcher3;
18
19import static android.app.Activity.RESULT_CANCELED;
20
21import android.appwidget.AppWidgetHost;
22import android.appwidget.AppWidgetHostView;
23import android.appwidget.AppWidgetManager;
24import android.appwidget.AppWidgetProviderInfo;
25import android.content.ActivityNotFoundException;
26import android.content.Context;
27import android.content.Intent;
28import android.os.Handler;
29import android.util.Log;
30import android.util.SparseArray;
31import android.view.LayoutInflater;
32import android.widget.Toast;
33
34import com.android.launcher3.config.FeatureFlags;
35import com.android.launcher3.widget.DeferredAppWidgetHostView;
36import com.android.launcher3.widget.LauncherAppWidgetHostView;
37
38import java.util.ArrayList;
39
40
41/**
42 * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView}
43 * which correctly captures all long-press events. This ensures that users can
44 * always pick up and move widgets.
45 */
46public class LauncherAppWidgetHost extends AppWidgetHost {
47
48    private static final int FLAG_LISTENING = 1;
49    private static final int FLAG_RESUMED = 1 << 1;
50    private static final int FLAG_LISTEN_IF_RESUMED = 1 << 2;
51
52    public static final int APPWIDGET_HOST_ID = 1024;
53
54    private final ArrayList<ProviderChangedListener> mProviderChangeListeners = new ArrayList<>();
55    private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
56
57    private final Context mContext;
58    private int mFlags = FLAG_RESUMED;
59
60    public LauncherAppWidgetHost(Context context) {
61        super(context, APPWIDGET_HOST_ID);
62        mContext = context;
63    }
64
65    @Override
66    protected LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
67            AppWidgetProviderInfo appWidget) {
68        LauncherAppWidgetHostView view = new LauncherAppWidgetHostView(context);
69        mViews.put(appWidgetId, view);
70        return view;
71    }
72
73    @Override
74    public void startListening() {
75        if (FeatureFlags.GO_DISABLE_WIDGETS) {
76            return;
77        }
78        mFlags |= FLAG_LISTENING;
79        try {
80            super.startListening();
81        } catch (Exception e) {
82            if (!Utilities.isBinderSizeError(e)) {
83                throw new RuntimeException(e);
84            }
85            // We're willing to let this slide. The exception is being caused by the list of
86            // RemoteViews which is being passed back. The startListening relationship will
87            // have been established by this point, and we will end up populating the
88            // widgets upon bind anyway. See issue 14255011 for more context.
89        }
90
91        // We go in reverse order and inflate any deferred widget
92        for (int i = mViews.size() - 1; i >= 0; i--) {
93            LauncherAppWidgetHostView view = mViews.valueAt(i);
94            if (view instanceof DeferredAppWidgetHostView) {
95                view.reInflate();
96            }
97        }
98    }
99
100    @Override
101    public void stopListening() {
102        if (FeatureFlags.GO_DISABLE_WIDGETS) {
103            return;
104        }
105        mFlags &= ~FLAG_LISTENING;
106        super.stopListening();
107    }
108
109    /**
110     * Updates the resumed state of the host.
111     * When a host is not resumed, it defers calls to startListening until host is resumed again.
112     * But if the host was already listening, it will not call stopListening.
113     *
114     * @see #setListenIfResumed(boolean)
115     */
116    public void setResumed(boolean isResumed) {
117        if (isResumed == ((mFlags & FLAG_RESUMED) != 0)) {
118            return;
119        }
120        if (isResumed) {
121            mFlags |= FLAG_RESUMED;
122            // Start listening if we were supposed to start listening on resume
123            if ((mFlags & FLAG_LISTEN_IF_RESUMED) != 0 && (mFlags & FLAG_LISTENING) == 0) {
124                startListening();
125            }
126        } else {
127            mFlags &= ~FLAG_RESUMED;
128        }
129    }
130
131    /**
132     * Updates the listening state of the host. If the host is not resumed, startListening is
133     * deferred until next resume.
134     *
135     * @see #setResumed(boolean)
136     */
137    public void setListenIfResumed(boolean listenIfResumed) {
138        if (!Utilities.ATLEAST_NOUGAT_MR1) {
139            return;
140        }
141        if (listenIfResumed == ((mFlags & FLAG_LISTEN_IF_RESUMED) != 0)) {
142            return;
143        }
144        if (listenIfResumed) {
145            mFlags |= FLAG_LISTEN_IF_RESUMED;
146            if ((mFlags & FLAG_RESUMED) != 0) {
147                // If we are resumed, start listening immediately. Note we do not check for
148                // duplicate calls before calling startListening as startListening is safe to call
149                // multiple times.
150                startListening();
151            }
152        } else {
153            mFlags &= ~FLAG_LISTEN_IF_RESUMED;
154            stopListening();
155        }
156    }
157
158    @Override
159    public int allocateAppWidgetId() {
160        if (FeatureFlags.GO_DISABLE_WIDGETS) {
161            return AppWidgetManager.INVALID_APPWIDGET_ID;
162        }
163
164        return super.allocateAppWidgetId();
165    }
166
167    public void addProviderChangeListener(ProviderChangedListener callback) {
168        mProviderChangeListeners.add(callback);
169    }
170
171    public void removeProviderChangeListener(ProviderChangedListener callback) {
172        mProviderChangeListeners.remove(callback);
173    }
174
175    protected void onProvidersChanged() {
176        if (!mProviderChangeListeners.isEmpty()) {
177            for (ProviderChangedListener callback : new ArrayList<>(mProviderChangeListeners)) {
178                callback.notifyWidgetProvidersChanged();
179            }
180        }
181    }
182
183    public AppWidgetHostView createView(Context context, int appWidgetId,
184            LauncherAppWidgetProviderInfo appWidget) {
185        if (appWidget.isCustomWidget()) {
186            LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
187            LayoutInflater inflater = (LayoutInflater)
188                    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
189            inflater.inflate(appWidget.initialLayout, lahv);
190            lahv.setAppWidget(0, appWidget);
191            return lahv;
192        } else if ((mFlags & FLAG_LISTENING) == 0) {
193            DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context);
194            view.setAppWidget(appWidgetId, appWidget);
195            mViews.put(appWidgetId, view);
196            return view;
197        } else {
198            try {
199                return super.createView(context, appWidgetId, appWidget);
200            } catch (Exception e) {
201                if (!Utilities.isBinderSizeError(e)) {
202                    throw new RuntimeException(e);
203                }
204
205                // If the exception was thrown while fetching the remote views, let the view stay.
206                // This will ensure that if the widget posts a valid update later, the view
207                // will update.
208                LauncherAppWidgetHostView view = mViews.get(appWidgetId);
209                if (view == null) {
210                    view = onCreateView(mContext, appWidgetId, appWidget);
211                }
212                view.setAppWidget(appWidgetId, appWidget);
213                view.switchToErrorView();
214                return  view;
215            }
216        }
217    }
218
219    /**
220     * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk.
221     */
222    @Override
223    protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) {
224        LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
225                mContext, appWidget);
226        super.onProviderChanged(appWidgetId, info);
227        // The super method updates the dimensions of the providerInfo. Update the
228        // launcher spans accordingly.
229        info.initSpans(mContext);
230    }
231
232    @Override
233    public void deleteAppWidgetId(int appWidgetId) {
234        super.deleteAppWidgetId(appWidgetId);
235        mViews.remove(appWidgetId);
236    }
237
238    @Override
239    public void clearViews() {
240        super.clearViews();
241        mViews.clear();
242    }
243
244    public void startBindFlow(BaseActivity activity,
245            int appWidgetId, AppWidgetProviderInfo info, int requestCode) {
246
247        if (FeatureFlags.GO_DISABLE_WIDGETS) {
248            sendActionCancelled(activity, requestCode);
249            return;
250        }
251
252        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
253                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
254                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
255                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile());
256        // TODO: we need to make sure that this accounts for the options bundle.
257        // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
258        activity.startActivityForResult(intent, requestCode);
259    }
260
261
262    public void startConfigActivity(BaseActivity activity, int widgetId, int requestCode) {
263        if (FeatureFlags.GO_DISABLE_WIDGETS) {
264            sendActionCancelled(activity, requestCode);
265            return;
266        }
267
268        try {
269            startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode, null);
270        } catch (ActivityNotFoundException | SecurityException e) {
271            Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
272            sendActionCancelled(activity, requestCode);
273        }
274    }
275
276    private void sendActionCancelled(final BaseActivity activity, final int requestCode) {
277        new Handler().post(() -> activity.onActivityResult(requestCode, RESULT_CANCELED, null));
278    }
279
280    /**
281     * Listener for getting notifications on provider changes.
282     */
283    public interface ProviderChangedListener {
284
285        void notifyWidgetProvidersChanged();
286    }
287}
288