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 17 package com.android.launcher3.qsb; 18 19 import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_BIND; 20 import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID; 21 import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_PROVIDER; 22 23 import android.app.Activity; 24 import android.app.Fragment; 25 import android.app.SearchManager; 26 import android.appwidget.AppWidgetHost; 27 import android.appwidget.AppWidgetHostView; 28 import android.appwidget.AppWidgetManager; 29 import android.appwidget.AppWidgetProviderInfo; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.os.Bundle; 34 import android.provider.Settings; 35 import android.util.AttributeSet; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.widget.FrameLayout; 40 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 44 import com.android.launcher3.InvariantDeviceProfile; 45 import com.android.launcher3.LauncherAppState; 46 import com.android.launcher3.R; 47 import com.android.launcher3.Utilities; 48 import com.android.launcher3.config.FeatureFlags; 49 import com.android.launcher3.graphics.FragmentWithPreview; 50 import com.android.launcher3.widget.util.WidgetSizes; 51 52 /** 53 * A frame layout which contains a QSB. This internally uses fragment to bind the view, which 54 * allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}. 55 * 56 * Note: WidgetManagerHelper can be disabled using FeatureFlags. In QSB, we should use 57 * AppWidgetManager directly, so that it keeps working in that case. 58 */ 59 public class QsbContainerView extends FrameLayout { 60 61 public static final String SEARCH_PROVIDER_SETTINGS_KEY = "SEARCH_PROVIDER_PACKAGE_NAME"; 62 63 /** 64 * Returns the package name for user configured search provider or from searchManager 65 * @param context 66 * @return String 67 */ 68 @Nullable getSearchWidgetPackageName(@onNull Context context)69 public static String getSearchWidgetPackageName(@NonNull Context context) { 70 String providerPkg = Settings.Global.getString(context.getContentResolver(), 71 SEARCH_PROVIDER_SETTINGS_KEY); 72 if (providerPkg == null) { 73 SearchManager searchManager = context.getSystemService(SearchManager.class); 74 ComponentName componentName = searchManager.getGlobalSearchActivity(); 75 if (componentName != null) { 76 providerPkg = searchManager.getGlobalSearchActivity().getPackageName(); 77 } 78 } 79 return providerPkg; 80 } 81 82 /** 83 * returns it's AppWidgetProviderInfo using package name from getSearchWidgetPackageName 84 * @param context 85 * @return AppWidgetProviderInfo 86 */ 87 @Nullable getSearchWidgetProviderInfo(@onNull Context context)88 public static AppWidgetProviderInfo getSearchWidgetProviderInfo(@NonNull Context context) { 89 String providerPkg = getSearchWidgetPackageName(context); 90 if (providerPkg == null) { 91 return null; 92 } 93 94 AppWidgetProviderInfo defaultWidgetForSearchPackage = null; 95 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); 96 for (AppWidgetProviderInfo info : 97 appWidgetManager.getInstalledProvidersForPackage(providerPkg, null)) { 98 if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) { 99 if ((info.widgetCategory 100 & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) { 101 return info; 102 } else if (defaultWidgetForSearchPackage == null) { 103 defaultWidgetForSearchPackage = info; 104 } 105 } 106 } 107 return defaultWidgetForSearchPackage; 108 } 109 110 /** 111 * returns componentName for searchWidget if package name is known. 112 */ 113 @Nullable getSearchComponentName(@onNull Context context)114 public static ComponentName getSearchComponentName(@NonNull Context context) { 115 AppWidgetProviderInfo providerInfo = 116 QsbContainerView.getSearchWidgetProviderInfo(context); 117 if (providerInfo != null) { 118 return providerInfo.provider; 119 } else { 120 String pkgName = QsbContainerView.getSearchWidgetPackageName(context); 121 if (pkgName != null) { 122 //we don't know the class name yet. we'll put the package name as placeholder 123 return new ComponentName(pkgName, pkgName); 124 } 125 return null; 126 } 127 } 128 QsbContainerView(Context context)129 public QsbContainerView(Context context) { 130 super(context); 131 } 132 QsbContainerView(Context context, AttributeSet attrs)133 public QsbContainerView(Context context, AttributeSet attrs) { 134 super(context, attrs); 135 } 136 QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr)137 public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 138 super(context, attrs, defStyleAttr); 139 } 140 141 @Override setPadding(int left, int top, int right, int bottom)142 public void setPadding(int left, int top, int right, int bottom) { 143 super.setPadding(0, 0, 0, 0); 144 } 145 setPaddingUnchecked(int left, int top, int right, int bottom)146 protected void setPaddingUnchecked(int left, int top, int right, int bottom) { 147 super.setPadding(left, top, right, bottom); 148 } 149 150 /** 151 * A fragment to display the QSB. 152 */ 153 public static class QsbFragment extends FragmentWithPreview { 154 155 public static final int QSB_WIDGET_HOST_ID = 1026; 156 private static final int REQUEST_BIND_QSB = 1; 157 158 protected String mKeyWidgetId = "qsb_widget_id"; 159 private QsbWidgetHost mQsbWidgetHost; 160 protected AppWidgetProviderInfo mWidgetInfo; 161 private QsbWidgetHostView mQsb; 162 163 // We need to store the orientation here, due to a bug (b/64916689) that results in widgets 164 // being inflated in the wrong orientation. 165 private int mOrientation; 166 167 @Override onInit(Bundle savedInstanceState)168 public void onInit(Bundle savedInstanceState) { 169 mQsbWidgetHost = createHost(); 170 mOrientation = getContext().getResources().getConfiguration().orientation; 171 } 172 createHost()173 protected QsbWidgetHost createHost() { 174 return new QsbWidgetHost(getContext(), QSB_WIDGET_HOST_ID, 175 (c) -> new QsbWidgetHostView(c), this::rebindFragment); 176 } 177 178 private FrameLayout mWrapper; 179 180 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)181 public View onCreateView( 182 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 183 184 mWrapper = new FrameLayout(getContext()); 185 // Only add the view when enabled 186 if (isQsbEnabled()) { 187 mQsbWidgetHost.startListening(); 188 mWrapper.addView(createQsb(mWrapper)); 189 } 190 return mWrapper; 191 } 192 createQsb(ViewGroup container)193 private View createQsb(ViewGroup container) { 194 mWidgetInfo = getSearchWidgetProvider(); 195 if (mWidgetInfo == null) { 196 // There is no search provider, just show the default widget. 197 return getDefaultView(container, false /* show setup icon */); 198 } 199 Bundle opts = createBindOptions(); 200 Context context = getContext(); 201 AppWidgetManager widgetManager = AppWidgetManager.getInstance(context); 202 203 int widgetId = Utilities.getPrefs(context).getInt(mKeyWidgetId, -1); 204 AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId); 205 boolean isWidgetBound = (widgetInfo != null) && 206 widgetInfo.provider.equals(mWidgetInfo.provider); 207 208 int oldWidgetId = widgetId; 209 if (!isWidgetBound && !isInPreviewMode()) { 210 if (widgetId > -1) { 211 // widgetId is already bound and its not the correct provider. reset host. 212 mQsbWidgetHost.deleteHost(); 213 } 214 215 widgetId = mQsbWidgetHost.allocateAppWidgetId(); 216 isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed( 217 widgetId, mWidgetInfo.getProfile(), mWidgetInfo.provider, opts); 218 if (!isWidgetBound) { 219 mQsbWidgetHost.deleteAppWidgetId(widgetId); 220 widgetId = -1; 221 } 222 223 if (oldWidgetId != widgetId) { 224 saveWidgetId(widgetId); 225 } 226 } 227 228 if (isWidgetBound) { 229 mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(context, widgetId, 230 mWidgetInfo); 231 mQsb.setId(R.id.qsb_widget); 232 233 if (!isInPreviewMode()) { 234 if (!containsAll(AppWidgetManager.getInstance(context) 235 .getAppWidgetOptions(widgetId), opts)) { 236 mQsb.updateAppWidgetOptions(opts); 237 } 238 } 239 return mQsb; 240 } 241 242 // Return a default widget with setup icon. 243 return getDefaultView(container, true /* show setup icon */); 244 } 245 saveWidgetId(int widgetId)246 private void saveWidgetId(int widgetId) { 247 Utilities.getPrefs(getContext()).edit().putInt(mKeyWidgetId, widgetId).apply(); 248 } 249 250 @Override onActivityResult(int requestCode, int resultCode, Intent data)251 public void onActivityResult(int requestCode, int resultCode, Intent data) { 252 if (requestCode == REQUEST_BIND_QSB) { 253 if (resultCode == Activity.RESULT_OK) { 254 saveWidgetId(data.getIntExtra(EXTRA_APPWIDGET_ID, -1)); 255 rebindFragment(); 256 } else { 257 mQsbWidgetHost.deleteHost(); 258 } 259 } 260 } 261 262 @Override onResume()263 public void onResume() { 264 super.onResume(); 265 if (mQsb != null && mQsb.isReinflateRequired(mOrientation)) { 266 rebindFragment(); 267 } 268 } 269 270 @Override onDestroy()271 public void onDestroy() { 272 mQsbWidgetHost.stopListening(); 273 super.onDestroy(); 274 } 275 rebindFragment()276 private void rebindFragment() { 277 // Exit if the embedded qsb is disabled 278 if (!isQsbEnabled()) { 279 return; 280 } 281 282 if (mWrapper != null && getContext() != null) { 283 mWrapper.removeAllViews(); 284 mWrapper.addView(createQsb(mWrapper)); 285 } 286 } 287 isQsbEnabled()288 public boolean isQsbEnabled() { 289 return FeatureFlags.QSB_ON_FIRST_SCREEN; 290 } 291 createBindOptions()292 protected Bundle createBindOptions() { 293 InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext()); 294 return WidgetSizes.getWidgetSizeOptions(getContext(), mWidgetInfo.provider, 295 idp.numColumns, 1); 296 } 297 getDefaultView(ViewGroup container, boolean showSetupIcon)298 protected View getDefaultView(ViewGroup container, boolean showSetupIcon) { 299 // Return a default widget with setup icon. 300 View v = QsbWidgetHostView.getDefaultView(container); 301 if (showSetupIcon) { 302 View setupButton = v.findViewById(R.id.btn_qsb_setup); 303 setupButton.setVisibility(View.VISIBLE); 304 setupButton.setOnClickListener((v2) -> startActivityForResult( 305 new Intent(ACTION_APPWIDGET_BIND) 306 .putExtra(EXTRA_APPWIDGET_ID, mQsbWidgetHost.allocateAppWidgetId()) 307 .putExtra(EXTRA_APPWIDGET_PROVIDER, mWidgetInfo.provider), 308 REQUEST_BIND_QSB)); 309 } 310 return v; 311 } 312 313 314 /** 315 * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX} 316 * provided by the package from getSearchProviderPackageName 317 * If widgetCategory is not supported, or no such widget is found, returns the first widget 318 * provided by the package. 319 */ getSearchWidgetProvider()320 protected AppWidgetProviderInfo getSearchWidgetProvider() { 321 return getSearchWidgetProviderInfo(getContext()); 322 } 323 } 324 325 public static class QsbWidgetHost extends AppWidgetHost { 326 327 private final WidgetViewFactory mViewFactory; 328 private final WidgetProvidersUpdateCallback mWidgetsUpdateCallback; 329 QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory, WidgetProvidersUpdateCallback widgetProvidersUpdateCallback)330 public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory, 331 WidgetProvidersUpdateCallback widgetProvidersUpdateCallback) { 332 super(context, hostId); 333 mViewFactory = viewFactory; 334 mWidgetsUpdateCallback = widgetProvidersUpdateCallback; 335 } 336 QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory)337 public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory) { 338 this(context, hostId, viewFactory, null); 339 } 340 341 @Override onCreateView( Context context, int appWidgetId, AppWidgetProviderInfo appWidget)342 protected AppWidgetHostView onCreateView( 343 Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { 344 return mViewFactory.newView(context); 345 } 346 347 @Override onProvidersChanged()348 protected void onProvidersChanged() { 349 super.onProvidersChanged(); 350 if (mWidgetsUpdateCallback != null) { 351 mWidgetsUpdateCallback.onProvidersUpdated(); 352 } 353 } 354 } 355 356 public interface WidgetViewFactory { 357 newView(Context context)358 QsbWidgetHostView newView(Context context); 359 } 360 361 /** 362 * Callback interface for packages list update. 363 */ 364 @FunctionalInterface 365 public interface WidgetProvidersUpdateCallback { 366 /** 367 * Gets called when widget providers list changes 368 */ onProvidersUpdated()369 void onProvidersUpdated(); 370 } 371 372 /** 373 * Returns true if {@param original} contains all entries defined in {@param updates} and 374 * have the same value. 375 * The comparison uses {@link Object#equals(Object)} to compare the values. 376 */ containsAll(Bundle original, Bundle updates)377 private static boolean containsAll(Bundle original, Bundle updates) { 378 for (String key : updates.keySet()) { 379 Object value1 = updates.get(key); 380 Object value2 = original.get(key); 381 if (value1 == null) { 382 if (value2 != null) { 383 return false; 384 } 385 } else if (!value1.equals(value2)) { 386 return false; 387 } 388 } 389 return true; 390 } 391 392 } 393