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