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