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