blob: 289e0d84c03628afb41044c22cfcc14486188dba [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;
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -070035import android.provider.Settings;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070036import android.util.AttributeSet;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.FrameLayout;
41
Samuel Fufaca37b8a2019-08-19 17:04:36 -070042import androidx.annotation.NonNull;
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -070043import androidx.annotation.Nullable;
44
Sunny Goyal2e013ea2016-10-07 16:17:19 -070045import com.android.launcher3.AppWidgetResizeFrame;
46import com.android.launcher3.InvariantDeviceProfile;
47import com.android.launcher3.LauncherAppState;
48import com.android.launcher3.R;
49import com.android.launcher3.Utilities;
Louis Beginb07a3552017-02-27 14:33:11 +080050import com.android.launcher3.config.FeatureFlags;
Sunny Goyalef92b822018-11-21 14:12:00 -080051import com.android.launcher3.graphics.FragmentWithPreview;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070052
53/**
54 * A frame layout which contains a QSB. This internally uses fragment to bind the view, which
55 * allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}.
Sunny Goyal64a75aa2017-07-03 13:50:52 -070056 *
Sunny Goyal337c81f2019-12-10 12:19:13 -080057 * Note: WidgetManagerHelper can be disabled using FeatureFlags. In QSB, we should use
Sunny Goyal64a75aa2017-07-03 13:50:52 -070058 * AppWidgetManager directly, so that it keeps working in that case.
Sunny Goyalda4fe1a2016-05-26 16:05:17 -070059 */
60public class QsbContainerView extends FrameLayout {
61
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -070062 public static final String SEARCH_PROVIDER_SETTINGS_KEY = "SEARCH_PROVIDER_PACKAGE_NAME";
63
Samuel Fufaca37b8a2019-08-19 17:04:36 -070064 /**
65 * Returns the package name for user configured search provider or from searchManager
66 * @param context
67 * @return String
68 */
69 @Nullable
70 public static String getSearchWidgetPackageName(@NonNull Context context) {
71 String providerPkg = Settings.Global.getString(context.getContentResolver(),
72 SEARCH_PROVIDER_SETTINGS_KEY);
73 if (providerPkg == null) {
74 SearchManager searchManager = context.getSystemService(SearchManager.class);
75 ComponentName componentName = searchManager.getGlobalSearchActivity();
76 if (componentName != null) {
77 providerPkg = searchManager.getGlobalSearchActivity().getPackageName();
78 }
79 }
80 return providerPkg;
81 }
82
83 /**
84 * returns it's AppWidgetProviderInfo using package name from getSearchWidgetPackageName
85 * @param context
86 * @return AppWidgetProviderInfo
87 */
88 @Nullable
89 public static AppWidgetProviderInfo getSearchWidgetProviderInfo(@NonNull Context context) {
90 String providerPkg = getSearchWidgetPackageName(context);
91 if (providerPkg == null) {
92 return null;
93 }
94
95 AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
96 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
97 for (AppWidgetProviderInfo info :
98 appWidgetManager.getInstalledProvidersForPackage(providerPkg, null)) {
99 if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) {
100 if ((info.widgetCategory
101 & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
102 return info;
103 } else if (defaultWidgetForSearchPackage == null) {
104 defaultWidgetForSearchPackage = info;
105 }
106 }
107 }
108 return defaultWidgetForSearchPackage;
109 }
110
111 /**
112 * returns componentName for searchWidget if package name is known.
113 */
114 @Nullable
115 public static ComponentName getSearchComponentName(@NonNull Context context) {
116 AppWidgetProviderInfo providerInfo =
117 QsbContainerView.getSearchWidgetProviderInfo(context);
118 if (providerInfo != null) {
119 return providerInfo.provider;
120 } else {
121 String pkgName = QsbContainerView.getSearchWidgetPackageName(context);
122 if (pkgName != null) {
123 //we don't know the class name yet. we'll put the package name as placeholder
124 return new ComponentName(pkgName, pkgName);
125 }
126 return null;
127 }
128 }
129
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700130 public QsbContainerView(Context context) {
131 super(context);
132 }
133
134 public QsbContainerView(Context context, AttributeSet attrs) {
135 super(context, attrs);
136 }
137
138 public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
139 super(context, attrs, defStyleAttr);
140 }
141
142 @Override
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700143 public void setPadding(int left, int top, int right, int bottom) {
144 super.setPadding(0, 0, 0, 0);
145 }
146
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700147 protected void setPaddingUnchecked(int left, int top, int right, int bottom) {
148 super.setPadding(left, top, right, bottom);
149 }
150
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700151 /**
152 * A fragment to display the QSB.
153 */
Sunny Goyalef92b822018-11-21 14:12:00 -0800154 public static class QsbFragment extends FragmentWithPreview {
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700155
Sunny Goyal95cded52018-06-14 20:08:57 -0700156 public static final int QSB_WIDGET_HOST_ID = 1026;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700157 private static final int REQUEST_BIND_QSB = 1;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700158
Sunny Goyal95cded52018-06-14 20:08:57 -0700159 protected String mKeyWidgetId = "qsb_widget_id";
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700160 private QsbWidgetHost mQsbWidgetHost;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700161 private AppWidgetProviderInfo mWidgetInfo;
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700162 private QsbWidgetHostView mQsb;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700163
Jon Miranda11ee2f62017-09-20 10:18:41 -0700164 // We need to store the orientation here, due to a bug (b/64916689) that results in widgets
165 // being inflated in the wrong orientation.
166 private int mOrientation;
167
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700168 @Override
Sunny Goyalef92b822018-11-21 14:12:00 -0800169 public void onInit(Bundle savedInstanceState) {
Sunny Goyal95cded52018-06-14 20:08:57 -0700170 mQsbWidgetHost = createHost();
Jon Miranda11ee2f62017-09-20 10:18:41 -0700171 mOrientation = getContext().getResources().getConfiguration().orientation;
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700172 }
173
Sunny Goyal95cded52018-06-14 20:08:57 -0700174 protected QsbWidgetHost createHost() {
Sunny Goyalef92b822018-11-21 14:12:00 -0800175 return new QsbWidgetHost(getContext(), QSB_WIDGET_HOST_ID,
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -0700176 (c) -> new QsbWidgetHostView(c), this::rebindFragment);
Sunny Goyal95cded52018-06-14 20:08:57 -0700177 }
178
Sunny Goyal02d3d432016-06-03 13:37:01 -0700179 private FrameLayout mWrapper;
180
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700181 @Override
182 public View onCreateView(
183 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
184
Sunny Goyalef92b822018-11-21 14:12:00 -0800185 mWrapper = new FrameLayout(getContext());
Louis Beginb07a3552017-02-27 14:33:11 +0800186 // Only add the view when enabled
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700187 if (isQsbEnabled()) {
Samuel Fufaca37b8a2019-08-19 17:04:36 -0700188 mQsbWidgetHost.startListening();
Louis Beginb07a3552017-02-27 14:33:11 +0800189 mWrapper.addView(createQsb(mWrapper));
190 }
Sunny Goyal02d3d432016-06-03 13:37:01 -0700191 return mWrapper;
192 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700193
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700194 private View createQsb(ViewGroup container) {
Sunny Goyal95cded52018-06-14 20:08:57 -0700195 mWidgetInfo = getSearchWidgetProvider();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700196 if (mWidgetInfo == null) {
197 // There is no search provider, just show the default widget.
Sunny Goyal95cded52018-06-14 20:08:57 -0700198 return getDefaultView(container, false /* show setup icon */);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700199 }
Sunny Goyal95cded52018-06-14 20:08:57 -0700200 Bundle opts = createBindOptions();
Sunny Goyalef92b822018-11-21 14:12:00 -0800201 Context context = getContext();
202 AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700203
Sunny Goyalef92b822018-11-21 14:12:00 -0800204 int widgetId = Utilities.getPrefs(context).getInt(mKeyWidgetId, -1);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700205 AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId);
206 boolean isWidgetBound = (widgetInfo != null) &&
207 widgetInfo.provider.equals(mWidgetInfo.provider);
208
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700209 int oldWidgetId = widgetId;
Sunny Goyalef92b822018-11-21 14:12:00 -0800210 if (!isWidgetBound && !isInPreviewMode()) {
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700211 if (widgetId > -1) {
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700212 // widgetId is already bound and its not the correct provider. reset host.
213 mQsbWidgetHost.deleteHost();
214 }
215
216 widgetId = mQsbWidgetHost.allocateAppWidgetId();
Sunny Goyal64a75aa2017-07-03 13:50:52 -0700217 isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed(
218 widgetId, mWidgetInfo.getProfile(), mWidgetInfo.provider, opts);
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700219 if (!isWidgetBound) {
220 mQsbWidgetHost.deleteAppWidgetId(widgetId);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700221 widgetId = -1;
222 }
223
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700224 if (oldWidgetId != widgetId) {
225 saveWidgetId(widgetId);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700226 }
227 }
228
229 if (isWidgetBound) {
Samuel Fufaca37b8a2019-08-19 17:04:36 -0700230 mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(context, widgetId,
231 mWidgetInfo);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700232 mQsb.setId(R.id.qsb_widget);
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700233
Sunny Goyalef92b822018-11-21 14:12:00 -0800234 if (!isInPreviewMode()) {
Sunny Goyalaae6fbb2019-01-31 16:05:58 -0800235 if (!containsAll(AppWidgetManager.getInstance(context)
Sunny Goyalef92b822018-11-21 14:12:00 -0800236 .getAppWidgetOptions(widgetId), opts)) {
237 mQsb.updateAppWidgetOptions(opts);
238 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700239 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700240 return mQsb;
241 }
242
243 // Return a default widget with setup icon.
Sunny Goyal95cded52018-06-14 20:08:57 -0700244 return getDefaultView(container, true /* show setup icon */);
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700245 }
246
247 private void saveWidgetId(int widgetId) {
Sunny Goyalef92b822018-11-21 14:12:00 -0800248 Utilities.getPrefs(getContext()).edit().putInt(mKeyWidgetId, widgetId).apply();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700249 }
250
251 @Override
252 public void onActivityResult(int requestCode, int resultCode, Intent data) {
253 if (requestCode == REQUEST_BIND_QSB) {
254 if (resultCode == Activity.RESULT_OK) {
Sunny Goyal95cded52018-06-14 20:08:57 -0700255 saveWidgetId(data.getIntExtra(EXTRA_APPWIDGET_ID, -1));
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700256 rebindFragment();
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700257 } else {
258 mQsbWidgetHost.deleteHost();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700259 }
260 }
261 }
262
263 @Override
264 public void onResume() {
265 super.onResume();
Jon Miranda11ee2f62017-09-20 10:18:41 -0700266 if (mQsb != null && mQsb.isReinflateRequired(mOrientation)) {
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700267 rebindFragment();
268 }
269 }
270
271 @Override
272 public void onDestroy() {
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700273 mQsbWidgetHost.stopListening();
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700274 super.onDestroy();
275 }
276
277 private void rebindFragment() {
Louis Beginb07a3552017-02-27 14:33:11 +0800278 // Exit if the embedded qsb is disabled
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700279 if (!isQsbEnabled()) {
Louis Beginb07a3552017-02-27 14:33:11 +0800280 return;
281 }
282
Sunny Goyalef92b822018-11-21 14:12:00 -0800283 if (mWrapper != null && getContext() != null) {
Sunny Goyal02d3d432016-06-03 13:37:01 -0700284 mWrapper.removeAllViews();
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700285 mWrapper.addView(createQsb(mWrapper));
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700286 }
287 }
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700288
289 public boolean isQsbEnabled() {
Jon Miranda7143ba62019-03-15 09:00:05 -0700290 return FeatureFlags.QSB_ON_FIRST_SCREEN;
Sunny Goyald0f43ce2018-05-30 17:35:24 -0700291 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700292
Sunny Goyal95cded52018-06-14 20:08:57 -0700293 protected Bundle createBindOptions() {
Sunny Goyalef92b822018-11-21 14:12:00 -0800294 InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700295
Sunny Goyal95cded52018-06-14 20:08:57 -0700296 Bundle opts = new Bundle();
Sunny Goyalef92b822018-11-21 14:12:00 -0800297 Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(getContext(),
Sunny Goyal95cded52018-06-14 20:08:57 -0700298 idp.numColumns, 1, null);
299 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
300 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top);
301 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right);
302 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom);
303 return opts;
304 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700305
Sunny Goyal95cded52018-06-14 20:08:57 -0700306 protected View getDefaultView(ViewGroup container, boolean showSetupIcon) {
307 // Return a default widget with setup icon.
308 View v = QsbWidgetHostView.getDefaultView(container);
309 if (showSetupIcon) {
310 View setupButton = v.findViewById(R.id.btn_qsb_setup);
311 setupButton.setVisibility(View.VISIBLE);
312 setupButton.setOnClickListener((v2) -> startActivityForResult(
313 new Intent(ACTION_APPWIDGET_BIND)
314 .putExtra(EXTRA_APPWIDGET_ID, mQsbWidgetHost.allocateAppWidgetId())
315 .putExtra(EXTRA_APPWIDGET_PROVIDER, mWidgetInfo.provider),
316 REQUEST_BIND_QSB));
317 }
318 return v;
319 }
320
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -0700321
322 /**
Sunny Goyal95cded52018-06-14 20:08:57 -0700323 * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -0700324 * provided by the package from getSearchProviderPackageName
Sunny Goyal95cded52018-06-14 20:08:57 -0700325 * If widgetCategory is not supported, or no such widget is found, returns the first widget
326 * provided by the package.
327 */
328 protected AppWidgetProviderInfo getSearchWidgetProvider() {
Samuel Fufaca37b8a2019-08-19 17:04:36 -0700329 return getSearchWidgetProviderInfo(getContext());
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700330 }
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700331 }
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700332
Sunny Goyal95cded52018-06-14 20:08:57 -0700333 public static class QsbWidgetHost extends AppWidgetHost {
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700334
Sunny Goyal95cded52018-06-14 20:08:57 -0700335 private final WidgetViewFactory mViewFactory;
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -0700336 private final WidgetProvidersUpdateCallback mWidgetsUpdateCallback;
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700337
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -0700338 public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory,
339 WidgetProvidersUpdateCallback widgetProvidersUpdateCallback) {
Sunny Goyal95cded52018-06-14 20:08:57 -0700340 super(context, hostId);
341 mViewFactory = viewFactory;
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -0700342 mWidgetsUpdateCallback = widgetProvidersUpdateCallback;
343 }
344
345 public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory) {
346 this(context, hostId, viewFactory, null);
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700347 }
348
349 @Override
350 protected AppWidgetHostView onCreateView(
351 Context context, int appWidgetId, AppWidgetProviderInfo appWidget) {
Sunny Goyal95cded52018-06-14 20:08:57 -0700352 return mViewFactory.newView(context);
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700353 }
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -0700354
355 @Override
356 protected void onProvidersChanged() {
357 super.onProvidersChanged();
358 if (mWidgetsUpdateCallback != null) {
359 mWidgetsUpdateCallback.onProvidersUpdated();
360 }
361 }
Sunny Goyal2e013ea2016-10-07 16:17:19 -0700362 }
Sunny Goyal95cded52018-06-14 20:08:57 -0700363
364 public interface WidgetViewFactory {
365
366 QsbWidgetHostView newView(Context context);
367 }
Sunny Goyalaae6fbb2019-01-31 16:05:58 -0800368
369 /**
Samuel Fufa9ec8cfa2019-08-12 16:19:41 -0700370 * Callback interface for packages list update.
371 */
372 @FunctionalInterface
373 public interface WidgetProvidersUpdateCallback {
374 /**
375 * Gets called when widget providers list changes
376 */
377 void onProvidersUpdated();
378 }
379
380 /**
Sunny Goyalaae6fbb2019-01-31 16:05:58 -0800381 * Returns true if {@param original} contains all entries defined in {@param updates} and
382 * have the same value.
383 * The comparison uses {@link Object#equals(Object)} to compare the values.
384 */
385 private static boolean containsAll(Bundle original, Bundle updates) {
386 for (String key : updates.keySet()) {
387 Object value1 = updates.get(key);
388 Object value2 = original.get(key);
389 if (value1 == null) {
390 if (value2 != null) {
391 return false;
392 }
393 } else if (!value1.equals(value2)) {
394 return false;
395 }
396 }
397 return true;
398 }
Samuel Fufaca37b8a2019-08-19 17:04:36 -0700399
Sunny Goyalda4fe1a2016-05-26 16:05:17 -0700400}