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