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