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 android.app.Activity;
20 import android.app.Fragment;
21 import android.app.SearchManager;
22 import android.appwidget.AppWidgetHost;
23 import android.appwidget.AppWidgetHostView;
24 import android.appwidget.AppWidgetManager;
25 import android.appwidget.AppWidgetProviderInfo;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.graphics.Rect;
30 import android.os.Bundle;
31 import android.util.AttributeSet;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.widget.FrameLayout;
36 
37 import com.android.launcher3.AppWidgetResizeFrame;
38 import com.android.launcher3.InvariantDeviceProfile;
39 import com.android.launcher3.LauncherAppState;
40 import com.android.launcher3.R;
41 import com.android.launcher3.Utilities;
42 import com.android.launcher3.compat.AppWidgetManagerCompat;
43 import com.android.launcher3.config.FeatureFlags;
44 
45 /**
46  * A frame layout which contains a QSB. This internally uses fragment to bind the view, which
47  * allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}.
48  */
49 public class QsbContainerView extends FrameLayout {
50 
QsbContainerView(Context context)51     public QsbContainerView(Context context) {
52         super(context);
53     }
54 
QsbContainerView(Context context, AttributeSet attrs)55     public QsbContainerView(Context context, AttributeSet attrs) {
56         super(context, attrs);
57     }
58 
QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr)59     public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
60         super(context, attrs, defStyleAttr);
61     }
62 
63     @Override
setPadding(int left, int top, int right, int bottom)64     public void setPadding(int left, int top, int right, int bottom) {
65         super.setPadding(0, 0, 0, 0);
66     }
67 
68     /**
69      * A fragment to display the QSB.
70      */
71     public static class QsbFragment extends Fragment implements View.OnClickListener {
72 
73         private static final int REQUEST_BIND_QSB = 1;
74         private static final String QSB_WIDGET_ID = "qsb_widget_id";
75 
76         private QsbWidgetHost mQsbWidgetHost;
77         private AppWidgetProviderInfo mWidgetInfo;
78         private QsbWidgetHostView mQsb;
79 
80         @Override
onCreate(Bundle savedInstanceState)81         public void onCreate(Bundle savedInstanceState) {
82             super.onCreate(savedInstanceState);
83             mQsbWidgetHost = new QsbWidgetHost(getActivity());
84         }
85 
86         private FrameLayout mWrapper;
87 
88         @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)89         public View onCreateView(
90                 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
91 
92             mWrapper = new FrameLayout(getActivity());
93 
94             // Only add the view when enabled
95             if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
96                 mWrapper.addView(createQsb(mWrapper));
97             }
98             return mWrapper;
99         }
100 
createQsb(ViewGroup container)101         private View createQsb(ViewGroup container) {
102             Activity activity = getActivity();
103             mWidgetInfo = getSearchWidgetProvider(activity);
104             if (mWidgetInfo == null) {
105                 // There is no search provider, just show the default widget.
106                 return QsbWidgetHostView.getDefaultView(container);
107             }
108 
109             AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(activity);
110             InvariantDeviceProfile idp = LauncherAppState.getIDP(activity);
111 
112             Bundle opts = new Bundle();
113             Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(activity, idp.numColumns, 1, null);
114             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
115             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top);
116             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right);
117             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom);
118 
119             int widgetId = Utilities.getPrefs(activity).getInt(QSB_WIDGET_ID, -1);
120             AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId);
121             boolean isWidgetBound = (widgetInfo != null) &&
122                     widgetInfo.provider.equals(mWidgetInfo.provider);
123 
124             int oldWidgetId = widgetId;
125             if (!isWidgetBound) {
126                 if (widgetId > -1) {
127                     // widgetId is already bound and its not the correct provider. reset host.
128                     mQsbWidgetHost.deleteHost();
129                 }
130 
131                 widgetId = mQsbWidgetHost.allocateAppWidgetId();
132                 isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed(widgetId, mWidgetInfo, opts);
133                 if (!isWidgetBound) {
134                     mQsbWidgetHost.deleteAppWidgetId(widgetId);
135                     widgetId = -1;
136                 }
137 
138                 if (oldWidgetId != widgetId) {
139                     saveWidgetId(widgetId);
140                 }
141             }
142 
143             if (isWidgetBound) {
144                 mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(activity, widgetId, mWidgetInfo);
145                 mQsb.setId(R.id.qsb_widget);
146 
147                 if (!Utilities.containsAll(AppWidgetManager.getInstance(activity)
148                         .getAppWidgetOptions(widgetId), opts)) {
149                     mQsb.updateAppWidgetOptions(opts);
150                 }
151                 mQsb.setPadding(0, 0, 0, 0);
152                 mQsbWidgetHost.startListening();
153                 return mQsb;
154             }
155 
156             // Return a default widget with setup icon.
157             View v = QsbWidgetHostView.getDefaultView(container);
158             View setupButton = v.findViewById(R.id.btn_qsb_setup);
159             setupButton.setVisibility(View.VISIBLE);
160             setupButton.setOnClickListener(this);
161             return v;
162         }
163 
saveWidgetId(int widgetId)164         private void saveWidgetId(int widgetId) {
165             Utilities.getPrefs(getActivity()).edit().putInt(QSB_WIDGET_ID, widgetId).apply();
166         }
167 
168         @Override
onClick(View view)169         public void onClick(View view) {
170             // Start intent for bind the widget
171             Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
172             // Allocate a new widget id for QSB
173             intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mQsbWidgetHost.allocateAppWidgetId());
174             intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, mWidgetInfo.provider);
175             startActivityForResult(intent, REQUEST_BIND_QSB);
176         }
177 
178         @Override
onActivityResult(int requestCode, int resultCode, Intent data)179         public void onActivityResult(int requestCode, int resultCode, Intent data) {
180             if (requestCode == REQUEST_BIND_QSB) {
181                 if (resultCode == Activity.RESULT_OK) {
182                     saveWidgetId(data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1));
183                     rebindFragment();
184                 } else {
185                     mQsbWidgetHost.deleteHost();
186                 }
187             }
188         }
189 
190         @Override
onResume()191         public void onResume() {
192             super.onResume();
193             if (mQsb != null && mQsb.isReinflateRequired()) {
194                 rebindFragment();
195             }
196         }
197 
198         @Override
onDestroy()199         public void onDestroy() {
200             mQsbWidgetHost.stopListening();
201             super.onDestroy();
202         }
203 
rebindFragment()204         private void rebindFragment() {
205             // Exit if the embedded qsb is disabled
206             if (!FeatureFlags.QSB_ON_FIRST_SCREEN) {
207                 return;
208             }
209 
210             if (mWrapper != null && getActivity() != null) {
211                 mWrapper.removeAllViews();
212                 mWrapper.addView(createQsb(mWrapper));
213             }
214         }
215     }
216 
217     /**
218      * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
219      * provided by the same package which is set to be global search activity.
220      * If widgetCategory is not supported, or no such widget is found, returns the first widget
221      * provided by the package.
222      */
getSearchWidgetProvider(Context context)223     public static AppWidgetProviderInfo getSearchWidgetProvider(Context context) {
224         SearchManager searchManager =
225                 (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
226         ComponentName searchComponent = searchManager.getGlobalSearchActivity();
227         if (searchComponent == null) return null;
228         String providerPkg = searchComponent.getPackageName();
229 
230         AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
231 
232         AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
233         for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) {
234             if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) {
235                 if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
236                     return info;
237                 } else if (defaultWidgetForSearchPackage == null) {
238                     defaultWidgetForSearchPackage = info;
239                 }
240             }
241         }
242         return defaultWidgetForSearchPackage;
243     }
244 
245     private static class QsbWidgetHost extends AppWidgetHost {
246 
247         private static final int QSB_WIDGET_HOST_ID = 1026;
248 
QsbWidgetHost(Context context)249         public QsbWidgetHost(Context context) {
250             super(context, QSB_WIDGET_HOST_ID);
251         }
252 
253         @Override
onCreateView( Context context, int appWidgetId, AppWidgetProviderInfo appWidget)254         protected AppWidgetHostView onCreateView(
255                 Context context, int appWidgetId, AppWidgetProviderInfo appWidget) {
256             return new QsbWidgetHostView(context);
257         }
258     }
259 }
260