• 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 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