blob: 857ea05d617a13bf014043771150bc49a1d43e8a [file] [log] [blame]
Sunny Goyalda4fe1a2016-05-26 16:05:17 -07001/*
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
Sunny Goyal2e013ea2016-10-07 16:17:19 -070017package com.android.launcher3.qsb;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070018
Sunny Goyal95cded52018-06-14 20:08:57 -070019import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_BIND;
20import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
21import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_PROVIDER;
22
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070023import android.app.Activity;
24import android.app.Fragment;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070025import android.app.SearchManager;
Sunny Goyal2e013ea2016-10-07 16:17:19 -070026import android.appwidget.AppWidgetHost;
27import android.appwidget.AppWidgetHostView;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070028import android.appwidget.AppWidgetManager;
29import android.appwidget.AppWidgetProviderInfo;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070030import android.content.ComponentName;
31import android.content.Context;
32import android.content.Intent;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070033import android.graphics.Rect;
34import android.os.Bundle;
35import android.util.AttributeSet;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39import android.widget.FrameLayout;
40
Sunny Goyal2e013ea2016-10-07 16:17:19 -070041import com.android.launcher3.AppWidgetResizeFrame;
42import com.android.launcher3.InvariantDeviceProfile;
43import com.android.launcher3.LauncherAppState;
44import com.android.launcher3.R;
45import com.android.launcher3.Utilities;
Louis Beginb07a3552017-02-27 14:33:11 +080046import com.android.launcher3.config.FeatureFlags;
Sunny Goyalef92b822018-11-21 14:12:00 -080047import com.android.launcher3.graphics.FragmentWithPreview;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070048
49/**
50 * A frame layout which contains a QSB. This internally uses fragment to bind the view, which
51 * allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}.
Sunny Goyal64a75aa2017-07-03 13:50:52 -070052 *
53 * Note: AppWidgetManagerCompat can be disabled using FeatureFlags. In QSB, we should use
54 * AppWidgetManager directly, so that it keeps working in that case.
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070055 */
56public class QsbContainerView extends FrameLayout {
57
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070058 public QsbContainerView(Context context) {
59 super(context);
60 }
61
62 public QsbContainerView(Context context, AttributeSet attrs) {
63 super(context, attrs);
64 }
65
66 public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
67 super(context, attrs, defStyleAttr);
68 }
69
70 @Override
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070071 public void setPadding(int left, int top, int right, int bottom) {
72 super.setPadding(0, 0, 0, 0);
73 }
74
Sunny Goyald0f43ce2018-05-30 17:35:24 -070075 protected void setPaddingUnchecked(int left, int top, int right, int bottom) {
76 super.setPadding(left, top, right, bottom);
77 }
78
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070079 /**
80 * A fragment to display the QSB.
81 */
Sunny Goyalef92b822018-11-21 14:12:00 -080082 public static class QsbFragment extends FragmentWithPreview {
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070083
Sunny Goyal95cded52018-06-14 20:08:57 -070084 public static final int QSB_WIDGET_HOST_ID = 1026;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070085 private static final int REQUEST_BIND_QSB = 1;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070086
Sunny Goyal95cded52018-06-14 20:08:57 -070087 protected String mKeyWidgetId = "qsb_widget_id";
Sunny Goyal2e013ea2016-10-07 16:17:19 -070088 private QsbWidgetHost mQsbWidgetHost;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070089 private AppWidgetProviderInfo mWidgetInfo;
Sunny Goyal2e013ea2016-10-07 16:17:19 -070090 private QsbWidgetHostView mQsb;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070091
Jon Miranda11ee2f62017-09-20 10:18:41 -070092 // We need to store the orientation here, due to a bug (b/64916689) that results in widgets
93 // being inflated in the wrong orientation.
94 private int mOrientation;
95
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070096 @Override
Sunny Goyalef92b822018-11-21 14:12:00 -080097 public void onInit(Bundle savedInstanceState) {
Sunny Goyal95cded52018-06-14 20:08:57 -070098 mQsbWidgetHost = createHost();
Jon Miranda11ee2f62017-09-20 10:18:41 -070099 mOrientation = getContext().getResources().getConfiguration().orientation;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700100 }
101
Sunny Goyal95cded52018-06-14 20:08:57 -0700102 protected QsbWidgetHost createHost() {
Sunny Goyalef92b822018-11-21 14:12:00 -0800103 return new QsbWidgetHost(getContext(), QSB_WIDGET_HOST_ID,
Sunny Goyal95cded52018-06-14 20:08:57 -0700104 (c) -> new QsbWidgetHostView(c));
105 }
106
Sunny Goyal02d3d432016-06-03 13:37:01 -0700107 private FrameLayout mWrapper;
108
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700109 @Override
110 public View onCreateView(
111 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
112
Sunny Goyalef92b822018-11-21 14:12:00 -0800113 mWrapper = new FrameLayout(getContext());
Louis Beginb07a3552017-02-27 14:33:11 +0800114
115 // Only add the view when enabled
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700116 if (isQsbEnabled()) {
Louis Beginb07a3552017-02-27 14:33:11 +0800117 mWrapper.addView(createQsb(mWrapper));
118 }
Sunny Goyal02d3d432016-06-03 13:37:01 -0700119 return mWrapper;
120 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700121
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700122 private View createQsb(ViewGroup container) {
Sunny Goyal95cded52018-06-14 20:08:57 -0700123 mWidgetInfo = getSearchWidgetProvider();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700124 if (mWidgetInfo == null) {
125 // There is no search provider, just show the default widget.
Sunny Goyal95cded52018-06-14 20:08:57 -0700126 return getDefaultView(container, false /* show setup icon */);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700127 }
Sunny Goyal95cded52018-06-14 20:08:57 -0700128 Bundle opts = createBindOptions();
Sunny Goyalef92b822018-11-21 14:12:00 -0800129 Context context = getContext();
130 AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700131
Sunny Goyalef92b822018-11-21 14:12:00 -0800132 int widgetId = Utilities.getPrefs(context).getInt(mKeyWidgetId, -1);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700133 AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId);
134 boolean isWidgetBound = (widgetInfo != null) &&
135 widgetInfo.provider.equals(mWidgetInfo.provider);
136
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700137 int oldWidgetId = widgetId;
Sunny Goyalef92b822018-11-21 14:12:00 -0800138 if (!isWidgetBound && !isInPreviewMode()) {
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700139 if (widgetId > -1) {
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700140 // widgetId is already bound and its not the correct provider. reset host.
141 mQsbWidgetHost.deleteHost();
142 }
143
144 widgetId = mQsbWidgetHost.allocateAppWidgetId();
Sunny Goyal64a75aa2017-07-03 13:50:52 -0700145 isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed(
146 widgetId, mWidgetInfo.getProfile(), mWidgetInfo.provider, opts);
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700147 if (!isWidgetBound) {
148 mQsbWidgetHost.deleteAppWidgetId(widgetId);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700149 widgetId = -1;
150 }
151
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700152 if (oldWidgetId != widgetId) {
153 saveWidgetId(widgetId);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700154 }
155 }
156
157 if (isWidgetBound) {
Sunny Goyalef92b822018-11-21 14:12:00 -0800158 mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(context, widgetId, mWidgetInfo);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700159 mQsb.setId(R.id.qsb_widget);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700160
Sunny Goyalef92b822018-11-21 14:12:00 -0800161 if (!isInPreviewMode()) {
Sunny Goyalaae6fbb2019-01-31 16:05:58 -0800162 if (!containsAll(AppWidgetManager.getInstance(context)
Sunny Goyalef92b822018-11-21 14:12:00 -0800163 .getAppWidgetOptions(widgetId), opts)) {
164 mQsb.updateAppWidgetOptions(opts);
165 }
166 mQsbWidgetHost.startListening();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700167 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700168 return mQsb;
169 }
170
171 // Return a default widget with setup icon.
Sunny Goyal95cded52018-06-14 20:08:57 -0700172 return getDefaultView(container, true /* show setup icon */);
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700173 }
174
175 private void saveWidgetId(int widgetId) {
Sunny Goyalef92b822018-11-21 14:12:00 -0800176 Utilities.getPrefs(getContext()).edit().putInt(mKeyWidgetId, widgetId).apply();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700177 }
178
179 @Override
180 public void onActivityResult(int requestCode, int resultCode, Intent data) {
181 if (requestCode == REQUEST_BIND_QSB) {
182 if (resultCode == Activity.RESULT_OK) {
Sunny Goyal95cded52018-06-14 20:08:57 -0700183 saveWidgetId(data.getIntExtra(EXTRA_APPWIDGET_ID, -1));
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700184 rebindFragment();
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700185 } else {
186 mQsbWidgetHost.deleteHost();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700187 }
188 }
189 }
190
191 @Override
192 public void onResume() {
193 super.onResume();
Jon Miranda11ee2f62017-09-20 10:18:41 -0700194 if (mQsb != null && mQsb.isReinflateRequired(mOrientation)) {
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700195 rebindFragment();
196 }
197 }
198
199 @Override
200 public void onDestroy() {
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700201 mQsbWidgetHost.stopListening();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700202 super.onDestroy();
203 }
204
205 private void rebindFragment() {
Louis Beginb07a3552017-02-27 14:33:11 +0800206 // Exit if the embedded qsb is disabled
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700207 if (!isQsbEnabled()) {
Louis Beginb07a3552017-02-27 14:33:11 +0800208 return;
209 }
210
Sunny Goyalef92b822018-11-21 14:12:00 -0800211 if (mWrapper != null && getContext() != null) {
Sunny Goyal02d3d432016-06-03 13:37:01 -0700212 mWrapper.removeAllViews();
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700213 mWrapper.addView(createQsb(mWrapper));
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700214 }
215 }
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700216
217 public boolean isQsbEnabled() {
Jon Miranda7143ba62019-03-15 09:00:05 -0700218 return FeatureFlags.QSB_ON_FIRST_SCREEN;
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700219 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700220
Sunny Goyal95cded52018-06-14 20:08:57 -0700221 protected Bundle createBindOptions() {
Sunny Goyalef92b822018-11-21 14:12:00 -0800222 InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700223
Sunny Goyal95cded52018-06-14 20:08:57 -0700224 Bundle opts = new Bundle();
Sunny Goyalef92b822018-11-21 14:12:00 -0800225 Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(getContext(),
Sunny Goyal95cded52018-06-14 20:08:57 -0700226 idp.numColumns, 1, null);
227 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
228 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top);
229 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right);
230 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom);
231 return opts;
232 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700233
Sunny Goyal95cded52018-06-14 20:08:57 -0700234 protected View getDefaultView(ViewGroup container, boolean showSetupIcon) {
235 // Return a default widget with setup icon.
236 View v = QsbWidgetHostView.getDefaultView(container);
237 if (showSetupIcon) {
238 View setupButton = v.findViewById(R.id.btn_qsb_setup);
239 setupButton.setVisibility(View.VISIBLE);
240 setupButton.setOnClickListener((v2) -> startActivityForResult(
241 new Intent(ACTION_APPWIDGET_BIND)
242 .putExtra(EXTRA_APPWIDGET_ID, mQsbWidgetHost.allocateAppWidgetId())
243 .putExtra(EXTRA_APPWIDGET_PROVIDER, mWidgetInfo.provider),
244 REQUEST_BIND_QSB));
245 }
246 return v;
247 }
248
249 /**
250 * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
251 * provided by the same package which is set to be global search activity.
252 * If widgetCategory is not supported, or no such widget is found, returns the first widget
253 * provided by the package.
254 */
255 protected AppWidgetProviderInfo getSearchWidgetProvider() {
256 SearchManager searchManager =
Sunny Goyalef92b822018-11-21 14:12:00 -0800257 (SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE);
Sunny Goyal95cded52018-06-14 20:08:57 -0700258 ComponentName searchComponent = searchManager.getGlobalSearchActivity();
259 if (searchComponent == null) return null;
260 String providerPkg = searchComponent.getPackageName();
261
262 AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
263
Sunny Goyalef92b822018-11-21 14:12:00 -0800264 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
Sunny Goyal95cded52018-06-14 20:08:57 -0700265 for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) {
266 if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) {
267 if ((info.widgetCategory
268 & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
269 return info;
270 } else if (defaultWidgetForSearchPackage == null) {
271 defaultWidgetForSearchPackage = info;
272 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700273 }
274 }
Sunny Goyal95cded52018-06-14 20:08:57 -0700275 return defaultWidgetForSearchPackage;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700276 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700277 }
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700278
Sunny Goyal95cded52018-06-14 20:08:57 -0700279 public static class QsbWidgetHost extends AppWidgetHost {
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700280
Sunny Goyal95cded52018-06-14 20:08:57 -0700281 private final WidgetViewFactory mViewFactory;
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700282
Sunny Goyal95cded52018-06-14 20:08:57 -0700283 public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory) {
284 super(context, hostId);
285 mViewFactory = viewFactory;
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700286 }
287
288 @Override
289 protected AppWidgetHostView onCreateView(
290 Context context, int appWidgetId, AppWidgetProviderInfo appWidget) {
Sunny Goyal95cded52018-06-14 20:08:57 -0700291 return mViewFactory.newView(context);
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700292 }
293 }
Sunny Goyal95cded52018-06-14 20:08:57 -0700294
295 public interface WidgetViewFactory {
296
297 QsbWidgetHostView newView(Context context);
298 }
Sunny Goyalaae6fbb2019-01-31 16:05:58 -0800299
300 /**
301 * Returns true if {@param original} contains all entries defined in {@param updates} and
302 * have the same value.
303 * The comparison uses {@link Object#equals(Object)} to compare the values.
304 */
305 private static boolean containsAll(Bundle original, Bundle updates) {
306 for (String key : updates.keySet()) {
307 Object value1 = updates.get(key);
308 Object value2 = original.get(key);
309 if (value1 == null) {
310 if (value2 != null) {
311 return false;
312 }
313 } else if (!value1.equals(value2)) {
314 return false;
315 }
316 }
317 return true;
318 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700319}