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.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 com.android.launcher3.AppWidgetResizeFrame; 42 import com.android.launcher3.InvariantDeviceProfile; 43 import com.android.launcher3.LauncherAppState; 44 import com.android.launcher3.R; 45 import com.android.launcher3.Utilities; 46 import com.android.launcher3.config.FeatureFlags; 47 import com.android.launcher3.graphics.FragmentWithPreview; 48 49 /** 50 * A frame layout which contains a QSB. This internally uses fragment to bind the view, which 51 * allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}. 52 * 53 * Note: AppWidgetManagerCompat can be disabled using FeatureFlags. In QSB, we should use 54 * AppWidgetManager directly, so that it keeps working in that case. 55 */ 56 public class QsbContainerView extends FrameLayout { 57 QsbContainerView(Context context)58 public QsbContainerView(Context context) { 59 super(context); 60 } 61 QsbContainerView(Context context, AttributeSet attrs)62 public QsbContainerView(Context context, AttributeSet attrs) { 63 super(context, attrs); 64 } 65 QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr)66 public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 67 super(context, attrs, defStyleAttr); 68 } 69 70 @Override setPadding(int left, int top, int right, int bottom)71 public void setPadding(int left, int top, int right, int bottom) { 72 super.setPadding(0, 0, 0, 0); 73 } 74 setPaddingUnchecked(int left, int top, int right, int bottom)75 protected void setPaddingUnchecked(int left, int top, int right, int bottom) { 76 super.setPadding(left, top, right, bottom); 77 } 78 79 /** 80 * A fragment to display the QSB. 81 */ 82 public static class QsbFragment extends FragmentWithPreview { 83 84 public static final int QSB_WIDGET_HOST_ID = 1026; 85 private static final int REQUEST_BIND_QSB = 1; 86 87 protected String mKeyWidgetId = "qsb_widget_id"; 88 private QsbWidgetHost mQsbWidgetHost; 89 private AppWidgetProviderInfo mWidgetInfo; 90 private QsbWidgetHostView mQsb; 91 92 // We need to store the orientation here, due to a bug (b/64916689) that results in widgets 93 // being inflated in the wrong orientation. 94 private int mOrientation; 95 96 @Override onInit(Bundle savedInstanceState)97 public void onInit(Bundle savedInstanceState) { 98 mQsbWidgetHost = createHost(); 99 mOrientation = getContext().getResources().getConfiguration().orientation; 100 } 101 createHost()102 protected QsbWidgetHost createHost() { 103 return new QsbWidgetHost(getContext(), QSB_WIDGET_HOST_ID, 104 (c) -> new QsbWidgetHostView(c)); 105 } 106 107 private FrameLayout mWrapper; 108 109 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)110 public View onCreateView( 111 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 112 113 mWrapper = new FrameLayout(getContext()); 114 115 // Only add the view when enabled 116 if (isQsbEnabled()) { 117 mWrapper.addView(createQsb(mWrapper)); 118 } 119 return mWrapper; 120 } 121 createQsb(ViewGroup container)122 private View createQsb(ViewGroup container) { 123 mWidgetInfo = getSearchWidgetProvider(); 124 if (mWidgetInfo == null) { 125 // There is no search provider, just show the default widget. 126 return getDefaultView(container, false /* show setup icon */); 127 } 128 Bundle opts = createBindOptions(); 129 Context context = getContext(); 130 AppWidgetManager widgetManager = AppWidgetManager.getInstance(context); 131 132 int widgetId = Utilities.getPrefs(context).getInt(mKeyWidgetId, -1); 133 AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId); 134 boolean isWidgetBound = (widgetInfo != null) && 135 widgetInfo.provider.equals(mWidgetInfo.provider); 136 137 int oldWidgetId = widgetId; 138 if (!isWidgetBound && !isInPreviewMode()) { 139 if (widgetId > -1) { 140 // widgetId is already bound and its not the correct provider. reset host. 141 mQsbWidgetHost.deleteHost(); 142 } 143 144 widgetId = mQsbWidgetHost.allocateAppWidgetId(); 145 isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed( 146 widgetId, mWidgetInfo.getProfile(), mWidgetInfo.provider, opts); 147 if (!isWidgetBound) { 148 mQsbWidgetHost.deleteAppWidgetId(widgetId); 149 widgetId = -1; 150 } 151 152 if (oldWidgetId != widgetId) { 153 saveWidgetId(widgetId); 154 } 155 } 156 157 if (isWidgetBound) { 158 mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(context, widgetId, mWidgetInfo); 159 mQsb.setId(R.id.qsb_widget); 160 161 if (!isInPreviewMode()) { 162 if (!containsAll(AppWidgetManager.getInstance(context) 163 .getAppWidgetOptions(widgetId), opts)) { 164 mQsb.updateAppWidgetOptions(opts); 165 } 166 mQsbWidgetHost.startListening(); 167 } 168 return mQsb; 169 } 170 171 // Return a default widget with setup icon. 172 return getDefaultView(container, true /* show setup icon */); 173 } 174 saveWidgetId(int widgetId)175 private void saveWidgetId(int widgetId) { 176 Utilities.getPrefs(getContext()).edit().putInt(mKeyWidgetId, widgetId).apply(); 177 } 178 179 @Override onActivityResult(int requestCode, int resultCode, Intent data)180 public void onActivityResult(int requestCode, int resultCode, Intent data) { 181 if (requestCode == REQUEST_BIND_QSB) { 182 if (resultCode == Activity.RESULT_OK) { 183 saveWidgetId(data.getIntExtra(EXTRA_APPWIDGET_ID, -1)); 184 rebindFragment(); 185 } else { 186 mQsbWidgetHost.deleteHost(); 187 } 188 } 189 } 190 191 @Override onResume()192 public void onResume() { 193 super.onResume(); 194 if (mQsb != null && mQsb.isReinflateRequired(mOrientation)) { 195 rebindFragment(); 196 } 197 } 198 199 @Override onDestroy()200 public void onDestroy() { 201 mQsbWidgetHost.stopListening(); 202 super.onDestroy(); 203 } 204 rebindFragment()205 private void rebindFragment() { 206 // Exit if the embedded qsb is disabled 207 if (!isQsbEnabled()) { 208 return; 209 } 210 211 if (mWrapper != null && getContext() != null) { 212 mWrapper.removeAllViews(); 213 mWrapper.addView(createQsb(mWrapper)); 214 } 215 } 216 isQsbEnabled()217 public boolean isQsbEnabled() { 218 return FeatureFlags.QSB_ON_FIRST_SCREEN; 219 } 220 createBindOptions()221 protected Bundle createBindOptions() { 222 InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext()); 223 224 Bundle opts = new Bundle(); 225 Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(getContext(), 226 idp.numColumns, 1, null); 227 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left); 228 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top); 229 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right); 230 opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom); 231 return opts; 232 } 233 getDefaultView(ViewGroup container, boolean showSetupIcon)234 protected View getDefaultView(ViewGroup container, boolean showSetupIcon) { 235 // Return a default widget with setup icon. 236 View v = QsbWidgetHostView.getDefaultView(container); 237 if (showSetupIcon) { 238 View setupButton = v.findViewById(R.id.btn_qsb_setup); 239 setupButton.setVisibility(View.VISIBLE); 240 setupButton.setOnClickListener((v2) -> startActivityForResult( 241 new Intent(ACTION_APPWIDGET_BIND) 242 .putExtra(EXTRA_APPWIDGET_ID, mQsbWidgetHost.allocateAppWidgetId()) 243 .putExtra(EXTRA_APPWIDGET_PROVIDER, mWidgetInfo.provider), 244 REQUEST_BIND_QSB)); 245 } 246 return v; 247 } 248 249 /** 250 * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX} 251 * provided by the same package which is set to be global search activity. 252 * If widgetCategory is not supported, or no such widget is found, returns the first widget 253 * provided by the package. 254 */ getSearchWidgetProvider()255 protected AppWidgetProviderInfo getSearchWidgetProvider() { 256 SearchManager searchManager = 257 (SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE); 258 ComponentName searchComponent = searchManager.getGlobalSearchActivity(); 259 if (searchComponent == null) return null; 260 String providerPkg = searchComponent.getPackageName(); 261 262 AppWidgetProviderInfo defaultWidgetForSearchPackage = null; 263 264 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext()); 265 for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) { 266 if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) { 267 if ((info.widgetCategory 268 & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) { 269 return info; 270 } else if (defaultWidgetForSearchPackage == null) { 271 defaultWidgetForSearchPackage = info; 272 } 273 } 274 } 275 return defaultWidgetForSearchPackage; 276 } 277 } 278 279 public static class QsbWidgetHost extends AppWidgetHost { 280 281 private final WidgetViewFactory mViewFactory; 282 QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory)283 public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory) { 284 super(context, hostId); 285 mViewFactory = viewFactory; 286 } 287 288 @Override onCreateView( Context context, int appWidgetId, AppWidgetProviderInfo appWidget)289 protected AppWidgetHostView onCreateView( 290 Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { 291 return mViewFactory.newView(context); 292 } 293 } 294 295 public interface WidgetViewFactory { 296 newView(Context context)297 QsbWidgetHostView newView(Context context); 298 } 299 300 /** 301 * Returns true if {@param original} contains all entries defined in {@param updates} and 302 * have the same value. 303 * The comparison uses {@link Object#equals(Object)} to compare the values. 304 */ containsAll(Bundle original, Bundle updates)305 private static boolean containsAll(Bundle original, Bundle updates) { 306 for (String key : updates.keySet()) { 307 Object value1 = updates.get(key); 308 Object value2 = original.get(key); 309 if (value1 == null) { 310 if (value2 != null) { 311 return false; 312 } 313 } else if (!value1.equals(value2)) { 314 return false; 315 } 316 } 317 return true; 318 } 319 } 320