1/*
2 * Copyright (C) 2016 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.qsb;
18
19import android.app.Activity;
20import android.app.Fragment;
21import android.app.SearchManager;
22import android.appwidget.AppWidgetHost;
23import android.appwidget.AppWidgetHostView;
24import android.appwidget.AppWidgetManager;
25import android.appwidget.AppWidgetProviderInfo;
26import android.content.ComponentName;
27import android.content.Context;
28import android.content.Intent;
29import android.graphics.Rect;
30import android.os.Bundle;
31import android.util.AttributeSet;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
35import android.widget.FrameLayout;
36
37import com.android.launcher3.AppWidgetResizeFrame;
38import com.android.launcher3.InvariantDeviceProfile;
39import com.android.launcher3.LauncherAppState;
40import com.android.launcher3.R;
41import com.android.launcher3.Utilities;
42import com.android.launcher3.config.FeatureFlags;
43
44/**
45 * A frame layout which contains a QSB. This internally uses fragment to bind the view, which
46 * allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}.
47 *
48 * Note: AppWidgetManagerCompat can be disabled using FeatureFlags. In QSB, we should use
49 * AppWidgetManager directly, so that it keeps working in that case.
50 */
51public class QsbContainerView extends FrameLayout {
52
53    public QsbContainerView(Context context) {
54        super(context);
55    }
56
57    public QsbContainerView(Context context, AttributeSet attrs) {
58        super(context, attrs);
59    }
60
61    public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
62        super(context, attrs, defStyleAttr);
63    }
64
65    @Override
66    public void setPadding(int left, int top, int right, int bottom) {
67        super.setPadding(0, 0, 0, 0);
68    }
69
70    protected void setPaddingUnchecked(int left, int top, int right, int bottom) {
71        super.setPadding(left, top, right, bottom);
72    }
73
74    /**
75     * A fragment to display the QSB.
76     */
77    public static class QsbFragment extends Fragment implements View.OnClickListener {
78
79        private static final int REQUEST_BIND_QSB = 1;
80        private static final String QSB_WIDGET_ID = "qsb_widget_id";
81
82        private QsbWidgetHost mQsbWidgetHost;
83        private AppWidgetProviderInfo mWidgetInfo;
84        private QsbWidgetHostView mQsb;
85
86        // We need to store the orientation here, due to a bug (b/64916689) that results in widgets
87        // being inflated in the wrong orientation.
88        private int mOrientation;
89
90        @Override
91        public void onCreate(Bundle savedInstanceState) {
92            super.onCreate(savedInstanceState);
93            mQsbWidgetHost = new QsbWidgetHost(getActivity());
94            mOrientation = getContext().getResources().getConfiguration().orientation;
95        }
96
97        private FrameLayout mWrapper;
98
99        @Override
100        public View onCreateView(
101                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
102
103            mWrapper = new FrameLayout(getActivity());
104
105            // Only add the view when enabled
106            if (isQsbEnabled()) {
107                mWrapper.addView(createQsb(mWrapper));
108            }
109            return mWrapper;
110        }
111
112        private View createQsb(ViewGroup container) {
113            Activity activity = getActivity();
114            mWidgetInfo = getSearchWidgetProvider(activity);
115            if (mWidgetInfo == null) {
116                // There is no search provider, just show the default widget.
117                return QsbWidgetHostView.getDefaultView(container);
118            }
119
120            AppWidgetManager widgetManager = AppWidgetManager.getInstance(activity);
121            InvariantDeviceProfile idp = LauncherAppState.getIDP(activity);
122
123            Bundle opts = new Bundle();
124            Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(activity, idp.numColumns, 1, null);
125            opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
126            opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top);
127            opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right);
128            opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom);
129
130            int widgetId = Utilities.getPrefs(activity).getInt(QSB_WIDGET_ID, -1);
131            AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId);
132            boolean isWidgetBound = (widgetInfo != null) &&
133                    widgetInfo.provider.equals(mWidgetInfo.provider);
134
135            int oldWidgetId = widgetId;
136            if (!isWidgetBound) {
137                if (widgetId > -1) {
138                    // widgetId is already bound and its not the correct provider. reset host.
139                    mQsbWidgetHost.deleteHost();
140                }
141
142                widgetId = mQsbWidgetHost.allocateAppWidgetId();
143                isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed(
144                        widgetId, mWidgetInfo.getProfile(), mWidgetInfo.provider, opts);
145                if (!isWidgetBound) {
146                    mQsbWidgetHost.deleteAppWidgetId(widgetId);
147                    widgetId = -1;
148                }
149
150                if (oldWidgetId != widgetId) {
151                    saveWidgetId(widgetId);
152                }
153            }
154
155            if (isWidgetBound) {
156                mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(activity, widgetId, mWidgetInfo);
157                mQsb.setId(R.id.qsb_widget);
158
159                if (!Utilities.containsAll(AppWidgetManager.getInstance(activity)
160                        .getAppWidgetOptions(widgetId), opts)) {
161                    mQsb.updateAppWidgetOptions(opts);
162                }
163                mQsb.setPadding(0, 0, 0, 0);
164                mQsbWidgetHost.startListening();
165                return mQsb;
166            }
167
168            // Return a default widget with setup icon.
169            View v = QsbWidgetHostView.getDefaultView(container);
170            View setupButton = v.findViewById(R.id.btn_qsb_setup);
171            setupButton.setVisibility(View.VISIBLE);
172            setupButton.setOnClickListener(this);
173            return v;
174        }
175
176        private void saveWidgetId(int widgetId) {
177            Utilities.getPrefs(getActivity()).edit().putInt(QSB_WIDGET_ID, widgetId).apply();
178        }
179
180        @Override
181        public void onClick(View view) {
182            // Start intent for bind the widget
183            Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
184            // Allocate a new widget id for QSB
185            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mQsbWidgetHost.allocateAppWidgetId());
186            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, mWidgetInfo.provider);
187            startActivityForResult(intent, REQUEST_BIND_QSB);
188        }
189
190        @Override
191        public void onActivityResult(int requestCode, int resultCode, Intent data) {
192            if (requestCode == REQUEST_BIND_QSB) {
193                if (resultCode == Activity.RESULT_OK) {
194                    saveWidgetId(data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1));
195                    rebindFragment();
196                } else {
197                    mQsbWidgetHost.deleteHost();
198                }
199            }
200        }
201
202        @Override
203        public void onResume() {
204            super.onResume();
205            if (mQsb != null && mQsb.isReinflateRequired(mOrientation)) {
206                rebindFragment();
207            }
208        }
209
210        @Override
211        public void onDestroy() {
212            mQsbWidgetHost.stopListening();
213            super.onDestroy();
214        }
215
216        private void rebindFragment() {
217            // Exit if the embedded qsb is disabled
218            if (!isQsbEnabled()) {
219                return;
220            }
221
222            if (mWrapper != null && getActivity() != null) {
223                mWrapper.removeAllViews();
224                mWrapper.addView(createQsb(mWrapper));
225            }
226        }
227
228        public boolean isQsbEnabled() {
229            return FeatureFlags.QSB_ON_FIRST_SCREEN;
230        }
231    }
232
233    /**
234     * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
235     * provided by the same package which is set to be global search activity.
236     * If widgetCategory is not supported, or no such widget is found, returns the first widget
237     * provided by the package.
238     */
239    public static AppWidgetProviderInfo getSearchWidgetProvider(Context context) {
240        SearchManager searchManager =
241                (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
242        ComponentName searchComponent = searchManager.getGlobalSearchActivity();
243        if (searchComponent == null) return null;
244        String providerPkg = searchComponent.getPackageName();
245
246        AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
247
248        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
249        for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) {
250            if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) {
251                if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
252                    return info;
253                } else if (defaultWidgetForSearchPackage == null) {
254                    defaultWidgetForSearchPackage = info;
255                }
256            }
257        }
258        return defaultWidgetForSearchPackage;
259    }
260
261    private static class QsbWidgetHost extends AppWidgetHost {
262
263        private static final int QSB_WIDGET_HOST_ID = 1026;
264
265        public QsbWidgetHost(Context context) {
266            super(context, QSB_WIDGET_HOST_ID);
267        }
268
269        @Override
270        protected AppWidgetHostView onCreateView(
271                Context context, int appWidgetId, AppWidgetProviderInfo appWidget) {
272            return new QsbWidgetHostView(context);
273        }
274    }
275}
276