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.graphics.Rect;
34 import android.os.Bundle;
35 import android.provider.Settings;
36 import android.util.AttributeSet;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.FrameLayout;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 
45 import com.android.launcher3.AppWidgetResizeFrame;
46 import com.android.launcher3.InvariantDeviceProfile;
47 import com.android.launcher3.LauncherAppState;
48 import com.android.launcher3.R;
49 import com.android.launcher3.Utilities;
50 import com.android.launcher3.config.FeatureFlags;
51 import com.android.launcher3.graphics.FragmentWithPreview;
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: AppWidgetManagerCompat 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     @Nullable
getSearchWidgetPackageName(@onNull Context context)70     public static String getSearchWidgetPackageName(@NonNull Context context) {
71         String providerPkg = Settings.Global.getString(context.getContentResolver(),
72                 SEARCH_PROVIDER_SETTINGS_KEY);
73         if (providerPkg == null) {
74             SearchManager searchManager = context.getSystemService(SearchManager.class);
75             ComponentName componentName = searchManager.getGlobalSearchActivity();
76             if (componentName != null) {
77                 providerPkg = searchManager.getGlobalSearchActivity().getPackageName();
78             }
79         }
80         return providerPkg;
81     }
82 
83     /**
84      * returns it's AppWidgetProviderInfo using package name from getSearchWidgetPackageName
85      * @param context
86      * @return AppWidgetProviderInfo
87      */
88     @Nullable
getSearchWidgetProviderInfo(@onNull Context context)89     public static AppWidgetProviderInfo getSearchWidgetProviderInfo(@NonNull Context context) {
90         String providerPkg = getSearchWidgetPackageName(context);
91         if (providerPkg == null) {
92             return null;
93         }
94 
95         AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
96         AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
97         for (AppWidgetProviderInfo info :
98                 appWidgetManager.getInstalledProvidersForPackage(providerPkg, null)) {
99             if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) {
100                 if ((info.widgetCategory
101                         & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
102                     return info;
103                 } else if (defaultWidgetForSearchPackage == null) {
104                     defaultWidgetForSearchPackage = info;
105                 }
106             }
107         }
108         return defaultWidgetForSearchPackage;
109     }
110 
111     /**
112      * returns componentName for searchWidget if package name is known.
113      */
114     @Nullable
getSearchComponentName(@onNull Context context)115     public static ComponentName getSearchComponentName(@NonNull  Context context) {
116         AppWidgetProviderInfo providerInfo =
117                 QsbContainerView.getSearchWidgetProviderInfo(context);
118         if (providerInfo != null) {
119             return providerInfo.provider;
120         } else {
121             String pkgName = QsbContainerView.getSearchWidgetPackageName(context);
122             if (pkgName != null) {
123                 //we don't know the class name yet. we'll put the package name as placeholder
124                 return new ComponentName(pkgName, pkgName);
125             }
126             return null;
127         }
128     }
129 
QsbContainerView(Context context)130     public QsbContainerView(Context context) {
131         super(context);
132     }
133 
QsbContainerView(Context context, AttributeSet attrs)134     public QsbContainerView(Context context, AttributeSet attrs) {
135         super(context, attrs);
136     }
137 
QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr)138     public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
139         super(context, attrs, defStyleAttr);
140     }
141 
142     @Override
setPadding(int left, int top, int right, int bottom)143     public void setPadding(int left, int top, int right, int bottom) {
144         super.setPadding(0, 0, 0, 0);
145     }
146 
setPaddingUnchecked(int left, int top, int right, int bottom)147     protected void setPaddingUnchecked(int left, int top, int right, int bottom) {
148         super.setPadding(left, top, right, bottom);
149     }
150 
151     /**
152      * A fragment to display the QSB.
153      */
154     public static class QsbFragment extends FragmentWithPreview {
155 
156         public static final int QSB_WIDGET_HOST_ID = 1026;
157         private static final int REQUEST_BIND_QSB = 1;
158 
159         protected String mKeyWidgetId = "qsb_widget_id";
160         private QsbWidgetHost mQsbWidgetHost;
161         private AppWidgetProviderInfo mWidgetInfo;
162         private QsbWidgetHostView mQsb;
163 
164         // We need to store the orientation here, due to a bug (b/64916689) that results in widgets
165         // being inflated in the wrong orientation.
166         private int mOrientation;
167 
168         @Override
onInit(Bundle savedInstanceState)169         public void onInit(Bundle savedInstanceState) {
170             mQsbWidgetHost = createHost();
171             mOrientation = getContext().getResources().getConfiguration().orientation;
172         }
173 
createHost()174         protected QsbWidgetHost createHost() {
175             return new QsbWidgetHost(getContext(), QSB_WIDGET_HOST_ID,
176                     (c) -> new QsbWidgetHostView(c), this::rebindFragment);
177         }
178 
179         private FrameLayout mWrapper;
180 
181         @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)182         public View onCreateView(
183                 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
184 
185             mWrapper = new FrameLayout(getContext());
186             // Only add the view when enabled
187             if (isQsbEnabled()) {
188                 mQsbWidgetHost.startListening();
189                 mWrapper.addView(createQsb(mWrapper));
190             }
191             return mWrapper;
192         }
193 
createQsb(ViewGroup container)194         private View createQsb(ViewGroup container) {
195             mWidgetInfo = getSearchWidgetProvider();
196             if (mWidgetInfo == null) {
197                 // There is no search provider, just show the default widget.
198                 return getDefaultView(container, false /* show setup icon */);
199             }
200             Bundle opts = createBindOptions();
201             Context context = getContext();
202             AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
203 
204             int widgetId = Utilities.getPrefs(context).getInt(mKeyWidgetId, -1);
205             AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId);
206             boolean isWidgetBound = (widgetInfo != null) &&
207                     widgetInfo.provider.equals(mWidgetInfo.provider);
208 
209             int oldWidgetId = widgetId;
210             if (!isWidgetBound && !isInPreviewMode()) {
211                 if (widgetId > -1) {
212                     // widgetId is already bound and its not the correct provider. reset host.
213                     mQsbWidgetHost.deleteHost();
214                 }
215 
216                 widgetId = mQsbWidgetHost.allocateAppWidgetId();
217                 isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed(
218                         widgetId, mWidgetInfo.getProfile(), mWidgetInfo.provider, opts);
219                 if (!isWidgetBound) {
220                     mQsbWidgetHost.deleteAppWidgetId(widgetId);
221                     widgetId = -1;
222                 }
223 
224                 if (oldWidgetId != widgetId) {
225                     saveWidgetId(widgetId);
226                 }
227             }
228 
229             if (isWidgetBound) {
230                 mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(context, widgetId,
231                         mWidgetInfo);
232                 mQsb.setId(R.id.qsb_widget);
233 
234                 if (!isInPreviewMode()) {
235                     if (!containsAll(AppWidgetManager.getInstance(context)
236                             .getAppWidgetOptions(widgetId), opts)) {
237                         mQsb.updateAppWidgetOptions(opts);
238                     }
239                 }
240                 return mQsb;
241             }
242 
243             // Return a default widget with setup icon.
244             return getDefaultView(container, true /* show setup icon */);
245         }
246 
saveWidgetId(int widgetId)247         private void saveWidgetId(int widgetId) {
248             Utilities.getPrefs(getContext()).edit().putInt(mKeyWidgetId, widgetId).apply();
249         }
250 
251         @Override
onActivityResult(int requestCode, int resultCode, Intent data)252         public void onActivityResult(int requestCode, int resultCode, Intent data) {
253             if (requestCode == REQUEST_BIND_QSB) {
254                 if (resultCode == Activity.RESULT_OK) {
255                     saveWidgetId(data.getIntExtra(EXTRA_APPWIDGET_ID, -1));
256                     rebindFragment();
257                 } else {
258                     mQsbWidgetHost.deleteHost();
259                 }
260             }
261         }
262 
263         @Override
onResume()264         public void onResume() {
265             super.onResume();
266             if (mQsb != null && mQsb.isReinflateRequired(mOrientation)) {
267                 rebindFragment();
268             }
269         }
270 
271         @Override
onDestroy()272         public void onDestroy() {
273             mQsbWidgetHost.stopListening();
274             super.onDestroy();
275         }
276 
rebindFragment()277         private void rebindFragment() {
278             // Exit if the embedded qsb is disabled
279             if (!isQsbEnabled()) {
280                 return;
281             }
282 
283             if (mWrapper != null && getContext() != null) {
284                 mWrapper.removeAllViews();
285                 mWrapper.addView(createQsb(mWrapper));
286             }
287         }
288 
isQsbEnabled()289         public boolean isQsbEnabled() {
290             return FeatureFlags.QSB_ON_FIRST_SCREEN;
291         }
292 
createBindOptions()293         protected Bundle createBindOptions() {
294             InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
295 
296             Bundle opts = new Bundle();
297             Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(getContext(),
298                     idp.numColumns, 1, null);
299             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
300             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top);
301             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right);
302             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom);
303             return opts;
304         }
305 
getDefaultView(ViewGroup container, boolean showSetupIcon)306         protected View getDefaultView(ViewGroup container, boolean showSetupIcon) {
307             // Return a default widget with setup icon.
308             View v = QsbWidgetHostView.getDefaultView(container);
309             if (showSetupIcon) {
310                 View setupButton = v.findViewById(R.id.btn_qsb_setup);
311                 setupButton.setVisibility(View.VISIBLE);
312                 setupButton.setOnClickListener((v2) -> startActivityForResult(
313                         new Intent(ACTION_APPWIDGET_BIND)
314                                 .putExtra(EXTRA_APPWIDGET_ID, mQsbWidgetHost.allocateAppWidgetId())
315                                 .putExtra(EXTRA_APPWIDGET_PROVIDER, mWidgetInfo.provider),
316                         REQUEST_BIND_QSB));
317             }
318             return v;
319         }
320 
321 
322         /**
323          * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
324          * provided by the package from getSearchProviderPackageName
325          * If widgetCategory is not supported, or no such widget is found, returns the first widget
326          * provided by the package.
327          */
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