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