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