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.graphics.Rect; 34 import android.os.Bundle; 35 import android.provider.Settings; 36 import android.util.AttributeSet; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.FrameLayout; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.Nullable; 44 45 import com.android.launcher3.AppWidgetResizeFrame; 46 import com.android.launcher3.InvariantDeviceProfile; 47 import com.android.launcher3.LauncherAppState; 48 import com.android.launcher3.R; 49 import com.android.launcher3.Utilities; 50 import com.android.launcher3.config.FeatureFlags; 51 import com.android.launcher3.graphics.FragmentWithPreview; 52 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)}. 56 * 57 * Note: AppWidgetManagerCompat can be disabled using FeatureFlags. In QSB, we should use 58 * AppWidgetManager directly, so that it keeps working in that case. 59 */ 60 public class QsbContainerView extends FrameLayout { 61 62 public static final String SEARCH_PROVIDER_SETTINGS_KEY = "SEARCH_PROVIDER_PACKAGE_NAME"; 63 64 /** 65 * Returns the package name for user configured search provider or from searchManager 66 * @param context 67 * @return String 68 */ 69 @Nullable getSearchWidgetPackageName(@onNull Context context)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 getSearchWidgetProviderInfo(@onNull Context context)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 getSearchComponentName(@onNull Context context)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 QsbContainerView(Context context)130 public QsbContainerView(Context context) { 131 super(context); 132 } 133 QsbContainerView(Context context, AttributeSet attrs)134 public QsbContainerView(Context context, AttributeSet attrs) { 135 super(context, attrs); 136 } 137 QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr)138 public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 139 super(context, attrs, defStyleAttr); 140 } 141 142 @Override setPadding(int left, int top, int right, int bottom)143 public void setPadding(int left, int top, int right, int bottom) { 144 super.setPadding(0, 0, 0, 0); 145 } 146 setPaddingUnchecked(int left, int top, int right, int bottom)147 protected void setPaddingUnchecked(int left, int top, int right, int bottom) { 148 super.setPadding(left, top, right, bottom); 149 } 150 151 /** 152 * A fragment to display the QSB. 153 */ 154 public static class QsbFragment extends FragmentWithPreview { 155 156 public static final int QSB_WIDGET_HOST_ID = 1026; 157 private static final int REQUEST_BIND_QSB = 1; 158 159 protected String mKeyWidgetId = "qsb_widget_id"; 160 private QsbWidgetHost mQsbWidgetHost; 161 private AppWidgetProviderInfo mWidgetInfo; 162 private QsbWidgetHostView mQsb; 163 164 // 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 168 @Override onInit(Bundle savedInstanceState)169 public void onInit(Bundle savedInstanceState) { 170 mQsbWidgetHost = createHost(); 171 mOrientation = getContext().getResources().getConfiguration().orientation; 172 } 173 createHost()174 protected QsbWidgetHost createHost() { 175 return new QsbWidgetHost(getContext(), QSB_WIDGET_HOST_ID, 176 (c) -> new QsbWidgetHostView(c), this::rebindFragment); 177 } 178 179 private FrameLayout mWrapper; 180 181 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)182 public View onCreateView( 183 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 184 185 mWrapper = new FrameLayout(getContext()); 186 // Only add the view when enabled 187 if (isQsbEnabled()) { 188 mQsbWidgetHost.startListening(); 189 mWrapper.addView(createQsb(mWrapper)); 190 } 191 return mWrapper; 192 } 193 createQsb(ViewGroup container)194 private View createQsb(ViewGroup container) { 195 mWidgetInfo = getSearchWidgetProvider(); 196 if (mWidgetInfo == null) { 197 // There is no search provider, just show the default widget. 198 return getDefaultView(container, false /* show setup icon */); 199 } 200 Bundle opts = createBindOptions(); 201 Context context = getContext(); 202 AppWidgetManager widgetManager = AppWidgetManager.getInstance(context); 203 204 int widgetId = Utilities.getPrefs(context).getInt(mKeyWidgetId, -1); 205 AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId); 206 boolean isWidgetBound = (widgetInfo != null) && 207 widgetInfo.provider.equals(mWidgetInfo.provider); 208 209 int oldWidgetId = widgetId; 210 if (!isWidgetBound && !isInPreviewMode()) { 211 if (widgetId > -1) { 212 // widgetId is already bound and its not the correct provider. reset host. 213 mQsbWidgetHost.deleteHost(); 214 } 215 216 widgetId = mQsbWidgetHost.allocateAppWidgetId(); 217 isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed( 218 widgetId, mWidgetInfo.getProfile(), mWidgetInfo.provider, opts); 219 if (!isWidgetBound) { 220 mQsbWidgetHost.deleteAppWidgetId(widgetId); 221 widgetId = -1; 222 } 223 224 if (oldWidgetId != widgetId) { 225 saveWidgetId(widgetId); 226 } 227 } 228 229 if (isWidgetBound) { 230 mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(context, widgetId, 231 mWidgetInfo); 232 mQsb.setId(R.id.qsb_widget); 233 234 if (!isInPreviewMode()) { 235 if (!containsAll(AppWidgetManager.getInstance(context) 236 .getAppWidgetOptions(widgetId), opts)) { 237 mQsb.updateAppWidgetOptions(opts); 238 } 239 } 240 return mQsb; 241 } 242 243 // Return a default widget with setup icon. 244 return getDefaultView(container, true /* show setup icon */); 245 } 246 saveWidgetId(int widgetId)247 private void saveWidgetId(int widgetId) { 248 Utilities.getPrefs(getContext()).edit().putInt(mKeyWidgetId, widgetId).apply(); 249 } 250 251 @Override onActivityResult(int requestCode, int resultCode, Intent data)252 public void onActivityResult(int requestCode, int resultCode, Intent data) { 253 if (requestCode == REQUEST_BIND_QSB) { 254 if (resultCode == Activity.RESULT_OK) { 255 saveWidgetId(data.getIntExtra(EXTRA_APPWIDGET_ID, -1)); 256 rebindFragment(); 257 } else { 258 mQsbWidgetHost.deleteHost(); 259 } 260 } 261 } 262 263 @Override onResume()264 public void onResume() { 265 super.onResume(); 266 if (mQsb != null && mQsb.isReinflateRequired(mOrientation)) { 267 rebindFragment(); 268 } 269 } 270 271 @Override onDestroy()272 public void onDestroy() { 273 mQsbWidgetHost.stopListening(); 274 super.onDestroy(); 275 } 276 rebindFragment()277 private void rebindFragment() { 278 // Exit if the embedded qsb is disabled 279 if (!isQsbEnabled()) { 280 return; 281 } 282 283 if (mWrapper != null && getContext() != null) { 284 mWrapper.removeAllViews(); 285 mWrapper.addView(createQsb(mWrapper)); 286 } 287 } 288 isQsbEnabled()289 public boolean isQsbEnabled() { 290 return FeatureFlags.QSB_ON_FIRST_SCREEN; 291 } 292 createBindOptions()293 protected Bundle createBindOptions() { 294 InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext()); 295 296 Bundle opts = new Bundle(); 297 Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(getContext(), 298 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 } 305 getDefaultView(ViewGroup container, boolean showSetupIcon)306 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 321 322 /** 323 * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX} 324 * provided by the package from getSearchProviderPackageName 325 * If widgetCategory is not supported, or no such widget is found, returns the first widget 326 * provided by the package. 327 */ 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