1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.systemui.statusbar.phone;
16 
17 import android.annotation.Nullable;
18 import android.content.Context;
19 import android.content.res.Configuration;
20 import android.graphics.drawable.Icon;
21 import android.util.AttributeSet;
22 import android.util.Log;
23 import android.util.SparseArray;
24 import android.view.Display;
25 import android.view.Display.Mode;
26 import android.view.Gravity;
27 import android.view.LayoutInflater;
28 import android.view.Surface;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.WindowManager;
32 import android.widget.FrameLayout;
33 import android.widget.LinearLayout;
34 import android.widget.Space;
35 
36 import com.android.systemui.Dependency;
37 import com.android.systemui.OverviewProxyService;
38 import com.android.systemui.R;
39 import com.android.systemui.plugins.PluginListener;
40 import com.android.systemui.plugins.PluginManager;
41 import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider;
42 import com.android.systemui.statusbar.phone.ReverseLinearLayout.ReverseRelativeLayout;
43 import com.android.systemui.statusbar.policy.KeyButtonView;
44 import com.android.systemui.tuner.TunerService;
45 import com.android.systemui.tuner.TunerService.Tunable;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.Objects;
50 
51 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
52 
53 public class NavigationBarInflaterView extends FrameLayout
54         implements Tunable, PluginListener<NavBarButtonProvider> {
55 
56     private static final String TAG = "NavBarInflater";
57 
58     public static final String NAV_BAR_VIEWS = "sysui_nav_bar";
59     public static final String NAV_BAR_LEFT = "sysui_nav_bar_left";
60     public static final String NAV_BAR_RIGHT = "sysui_nav_bar_right";
61 
62     public static final String MENU_IME_ROTATE = "menu_ime";
63     public static final String BACK = "back";
64     public static final String HOME = "home";
65     public static final String RECENT = "recent";
66     public static final String NAVSPACE = "space";
67     public static final String CLIPBOARD = "clipboard";
68     public static final String KEY = "key";
69     public static final String LEFT = "left";
70     public static final String RIGHT = "right";
71     public static final String CONTEXTUAL = "contextual";
72 
73     public static final String GRAVITY_SEPARATOR = ";";
74     public static final String BUTTON_SEPARATOR = ",";
75 
76     public static final String SIZE_MOD_START = "[";
77     public static final String SIZE_MOD_END = "]";
78 
79     public static final String KEY_CODE_START = "(";
80     public static final String KEY_IMAGE_DELIM = ":";
81     public static final String KEY_CODE_END = ")";
82     private static final String WEIGHT_SUFFIX = "W";
83     private static final String WEIGHT_CENTERED_SUFFIX = "WC";
84 
85     private final List<NavBarButtonProvider> mPlugins = new ArrayList<>();
86     private final Display mDisplay;
87 
88     protected LayoutInflater mLayoutInflater;
89     protected LayoutInflater mLandscapeInflater;
90 
91     protected FrameLayout mRot0;
92     protected FrameLayout mRot90;
93     private boolean isRot0Landscape;
94 
95     private SparseArray<ButtonDispatcher> mButtonDispatchers;
96     private String mCurrentLayout;
97 
98     private View mLastPortrait;
99     private View mLastLandscape;
100 
101     private boolean mAlternativeOrder;
102     private boolean mUsingCustomLayout;
103 
104     private OverviewProxyService mOverviewProxyService;
105 
NavigationBarInflaterView(Context context, AttributeSet attrs)106     public NavigationBarInflaterView(Context context, AttributeSet attrs) {
107         super(context, attrs);
108         createInflaters();
109         mDisplay = ((WindowManager)
110                 context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
111         Mode displayMode = mDisplay.getMode();
112         isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
113         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
114     }
115 
createInflaters()116     private void createInflaters() {
117         mLayoutInflater = LayoutInflater.from(mContext);
118         Configuration landscape = new Configuration();
119         landscape.setTo(mContext.getResources().getConfiguration());
120         landscape.orientation = Configuration.ORIENTATION_LANDSCAPE;
121         mLandscapeInflater = LayoutInflater.from(mContext.createConfigurationContext(landscape));
122     }
123 
124     @Override
onFinishInflate()125     protected void onFinishInflate() {
126         super.onFinishInflate();
127         inflateChildren();
128         clearViews();
129         inflateLayout(getDefaultLayout());
130     }
131 
inflateChildren()132     private void inflateChildren() {
133         removeAllViews();
134         mRot0 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout, this, false);
135         mRot0.setId(R.id.rot0);
136         addView(mRot0);
137         mRot90 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_rot90, this,
138                 false);
139         mRot90.setId(R.id.rot90);
140         addView(mRot90);
141         updateAlternativeOrder();
142     }
143 
getDefaultLayout()144     protected String getDefaultLayout() {
145         final int defaultResource = mOverviewProxyService.shouldShowSwipeUpUI()
146                 ? R.string.config_navBarLayoutQuickstep
147                 : R.string.config_navBarLayout;
148         return mContext.getString(defaultResource);
149     }
150 
151     @Override
onAttachedToWindow()152     protected void onAttachedToWindow() {
153         super.onAttachedToWindow();
154         Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
155                 NAV_BAR_RIGHT);
156         Dependency.get(PluginManager.class).addPluginListener(this,
157                 NavBarButtonProvider.class, true /* Allow multiple */);
158     }
159 
160     @Override
onDetachedFromWindow()161     protected void onDetachedFromWindow() {
162         Dependency.get(TunerService.class).removeTunable(this);
163         Dependency.get(PluginManager.class).removePluginListener(this);
164         super.onDetachedFromWindow();
165     }
166 
167     @Override
onTuningChanged(String key, String newValue)168     public void onTuningChanged(String key, String newValue) {
169         if (NAV_BAR_VIEWS.equals(key)) {
170             if (!Objects.equals(mCurrentLayout, newValue)) {
171                 mUsingCustomLayout = newValue != null;
172                 clearViews();
173                 inflateLayout(newValue);
174             }
175         } else if (NAV_BAR_LEFT.equals(key) || NAV_BAR_RIGHT.equals(key)) {
176             clearViews();
177             inflateLayout(mCurrentLayout);
178         }
179     }
180 
onLikelyDefaultLayoutChange()181     public void onLikelyDefaultLayoutChange() {
182         // Don't override custom layouts
183         if (mUsingCustomLayout) return;
184 
185         // Reevaluate new layout
186         final String newValue = getDefaultLayout();
187         if (!Objects.equals(mCurrentLayout, newValue)) {
188             clearViews();
189             inflateLayout(newValue);
190         }
191     }
192 
setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDispatchers)193     public void setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDispatchers) {
194         mButtonDispatchers = buttonDispatchers;
195         for (int i = 0; i < buttonDispatchers.size(); i++) {
196             initiallyFill(buttonDispatchers.valueAt(i));
197         }
198     }
199 
updateButtonDispatchersCurrentView()200     public void updateButtonDispatchersCurrentView() {
201         if (mButtonDispatchers != null) {
202             final int rotation = mDisplay.getRotation();
203             final boolean portrait = rotation == Surface.ROTATION_0
204                     || rotation == Surface.ROTATION_180;
205             final View view = portrait ? mRot0 : mRot90;
206             for (int i = 0; i < mButtonDispatchers.size(); i++) {
207                 final ButtonDispatcher dispatcher = mButtonDispatchers.valueAt(i);
208                 dispatcher.setCurrentView(view);
209             }
210         }
211     }
212 
setAlternativeOrder(boolean alternativeOrder)213     public void setAlternativeOrder(boolean alternativeOrder) {
214         if (alternativeOrder != mAlternativeOrder) {
215             mAlternativeOrder = alternativeOrder;
216             updateAlternativeOrder();
217         }
218     }
219 
updateAlternativeOrder()220     private void updateAlternativeOrder() {
221         updateAlternativeOrder(mRot0.findViewById(R.id.ends_group));
222         updateAlternativeOrder(mRot0.findViewById(R.id.center_group));
223         updateAlternativeOrder(mRot90.findViewById(R.id.ends_group));
224         updateAlternativeOrder(mRot90.findViewById(R.id.center_group));
225     }
226 
updateAlternativeOrder(View v)227     private void updateAlternativeOrder(View v) {
228         if (v instanceof ReverseLinearLayout) {
229             ((ReverseLinearLayout) v).setAlternativeOrder(mAlternativeOrder);
230         }
231     }
232 
initiallyFill(ButtonDispatcher buttonDispatcher)233     private void initiallyFill(ButtonDispatcher buttonDispatcher) {
234         addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.ends_group));
235         addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.center_group));
236         addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.ends_group));
237         addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.center_group));
238     }
239 
addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent)240     private void addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent) {
241         for (int i = 0; i < parent.getChildCount(); i++) {
242             // Need to manually search for each id, just in case each group has more than one
243             // of a single id.  It probably mostly a waste of time, but shouldn't take long
244             // and will only happen once.
245             if (parent.getChildAt(i).getId() == buttonDispatcher.getId()) {
246                 buttonDispatcher.addView(parent.getChildAt(i));
247             }
248             if (parent.getChildAt(i) instanceof ViewGroup) {
249                 addAll(buttonDispatcher, (ViewGroup) parent.getChildAt(i));
250             }
251         }
252     }
253 
inflateLayout(String newLayout)254     protected void inflateLayout(String newLayout) {
255         mCurrentLayout = newLayout;
256         if (newLayout == null) {
257             newLayout = getDefaultLayout();
258         }
259         String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
260         if (sets.length != 3) {
261             Log.d(TAG, "Invalid layout.");
262             newLayout = getDefaultLayout();
263             sets = newLayout.split(GRAVITY_SEPARATOR, 3);
264         }
265         String[] start = sets[0].split(BUTTON_SEPARATOR);
266         String[] center = sets[1].split(BUTTON_SEPARATOR);
267         String[] end = sets[2].split(BUTTON_SEPARATOR);
268         // Inflate these in start to end order or accessibility traversal will be messed up.
269         inflateButtons(start, mRot0.findViewById(R.id.ends_group), isRot0Landscape, true);
270         inflateButtons(start, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, true);
271 
272         inflateButtons(center, mRot0.findViewById(R.id.center_group), isRot0Landscape, false);
273         inflateButtons(center, mRot90.findViewById(R.id.center_group), !isRot0Landscape, false);
274 
275         addGravitySpacer(mRot0.findViewById(R.id.ends_group));
276         addGravitySpacer(mRot90.findViewById(R.id.ends_group));
277 
278         inflateButtons(end, mRot0.findViewById(R.id.ends_group), isRot0Landscape, false);
279         inflateButtons(end, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, false);
280 
281         updateButtonDispatchersCurrentView();
282     }
283 
addGravitySpacer(LinearLayout layout)284     private void addGravitySpacer(LinearLayout layout) {
285         layout.addView(new Space(mContext), new LinearLayout.LayoutParams(0, 0, 1));
286     }
287 
inflateButtons(String[] buttons, ViewGroup parent, boolean landscape, boolean start)288     private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape,
289             boolean start) {
290         for (int i = 0; i < buttons.length; i++) {
291             inflateButton(buttons[i], parent, landscape, start);
292         }
293     }
294 
copy(ViewGroup.LayoutParams layoutParams)295     private ViewGroup.LayoutParams copy(ViewGroup.LayoutParams layoutParams) {
296         if (layoutParams instanceof LinearLayout.LayoutParams) {
297             return new LinearLayout.LayoutParams(layoutParams.width, layoutParams.height,
298                     ((LinearLayout.LayoutParams) layoutParams).weight);
299         }
300         return new LayoutParams(layoutParams.width, layoutParams.height);
301     }
302 
303     @Nullable
inflateButton(String buttonSpec, ViewGroup parent, boolean landscape, boolean start)304     protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
305             boolean start) {
306         LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
307         View v = createView(buttonSpec, parent, inflater);
308         if (v == null) return null;
309 
310         v = applySize(v, buttonSpec, landscape, start);
311         parent.addView(v);
312         addToDispatchers(v);
313         View lastView = landscape ? mLastLandscape : mLastPortrait;
314         View accessibilityView = v;
315         if (v instanceof ReverseRelativeLayout) {
316             accessibilityView = ((ReverseRelativeLayout) v).getChildAt(0);
317         }
318         if (lastView != null) {
319             accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
320         }
321         if (landscape) {
322             mLastLandscape = accessibilityView;
323         } else {
324             mLastPortrait = accessibilityView;
325         }
326         return v;
327     }
328 
applySize(View v, String buttonSpec, boolean landscape, boolean start)329     private View applySize(View v, String buttonSpec, boolean landscape, boolean start) {
330         String sizeStr = extractSize(buttonSpec);
331         if (sizeStr == null) return v;
332 
333         if (sizeStr.contains(WEIGHT_SUFFIX)) {
334             // To support gravity, wrap in RelativeLayout and apply gravity to it.
335             // Children wanting to use gravity must be smaller then the frame.
336             float weight = Float.parseFloat(sizeStr.substring(0, sizeStr.indexOf(WEIGHT_SUFFIX)));
337             ReverseRelativeLayout frame = new ReverseRelativeLayout(mContext);
338             LayoutParams childParams = new LayoutParams(v.getLayoutParams());
339 
340             // Compute gravity to apply
341             int gravity = (landscape) ? (start ? Gravity.TOP : Gravity.BOTTOM)
342                     : (start ? Gravity.START : Gravity.END);
343             if (sizeStr.endsWith(WEIGHT_CENTERED_SUFFIX)) gravity = Gravity.CENTER;
344 
345             // Set default gravity, flipped if needed in reversed layouts (270 RTL and 90 LTR)
346             frame.setDefaultGravity(gravity);
347             frame.setGravity(gravity); // Apply gravity to root
348 
349             frame.addView(v, childParams);
350 
351             // Use weighting to set the width of the frame
352             frame.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, weight));
353 
354             // Ensure ripples can be drawn outside bounds
355             frame.setClipChildren(false);
356             frame.setClipToPadding(false);
357 
358             return frame;
359         }
360 
361         float size = Float.parseFloat(sizeStr);
362         ViewGroup.LayoutParams params = v.getLayoutParams();
363         params.width = (int) (params.width * size);
364         return v;
365     }
366 
createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater)367     private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
368         View v = null;
369         String button = extractButton(buttonSpec);
370         if (LEFT.equals(button)) {
371             String s = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, NAVSPACE);
372             button = extractButton(s);
373         } else if (RIGHT.equals(button)) {
374             String s = Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT, MENU_IME_ROTATE);
375             button = extractButton(s);
376         }
377         // Let plugins go first so they can override a standard view if they want.
378         for (NavBarButtonProvider provider : mPlugins) {
379             v = provider.createView(buttonSpec, parent);
380             if (v != null) return v;
381         }
382         if (HOME.equals(button)) {
383             v = inflater.inflate(R.layout.home, parent, false);
384         } else if (BACK.equals(button)) {
385             v = inflater.inflate(R.layout.back, parent, false);
386         } else if (RECENT.equals(button)) {
387             v = inflater.inflate(R.layout.recent_apps, parent, false);
388         } else if (MENU_IME_ROTATE.equals(button)) {
389             v = inflater.inflate(R.layout.menu_ime, parent, false);
390         } else if (NAVSPACE.equals(button)) {
391             v = inflater.inflate(R.layout.nav_key_space, parent, false);
392         } else if (CLIPBOARD.equals(button)) {
393             v = inflater.inflate(R.layout.clipboard, parent, false);
394         } else if (CONTEXTUAL.equals(button)) {
395             v = inflater.inflate(R.layout.contextual, parent, false);
396         } else if (button.startsWith(KEY)) {
397             String uri = extractImage(button);
398             int code = extractKeycode(button);
399             v = inflater.inflate(R.layout.custom_key, parent, false);
400             ((KeyButtonView) v).setCode(code);
401             if (uri != null) {
402                 if (uri.contains(":")) {
403                     ((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri));
404                 } else if (uri.contains("/")) {
405                     int index = uri.indexOf('/');
406                     String pkg = uri.substring(0, index);
407                     int id = Integer.parseInt(uri.substring(index + 1));
408                     ((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id));
409                 }
410             }
411         }
412         return v;
413     }
414 
extractImage(String buttonSpec)415     public static String extractImage(String buttonSpec) {
416         if (!buttonSpec.contains(KEY_IMAGE_DELIM)) {
417             return null;
418         }
419         final int start = buttonSpec.indexOf(KEY_IMAGE_DELIM);
420         String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_CODE_END));
421         return subStr;
422     }
423 
extractKeycode(String buttonSpec)424     public static int extractKeycode(String buttonSpec) {
425         if (!buttonSpec.contains(KEY_CODE_START)) {
426             return 1;
427         }
428         final int start = buttonSpec.indexOf(KEY_CODE_START);
429         String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_IMAGE_DELIM));
430         return Integer.parseInt(subStr);
431     }
432 
extractSize(String buttonSpec)433     public static String extractSize(String buttonSpec) {
434         if (!buttonSpec.contains(SIZE_MOD_START)) {
435             return null;
436         }
437         final int sizeStart = buttonSpec.indexOf(SIZE_MOD_START);
438         return buttonSpec.substring(sizeStart + 1, buttonSpec.indexOf(SIZE_MOD_END));
439     }
440 
extractButton(String buttonSpec)441     public static String extractButton(String buttonSpec) {
442         if (!buttonSpec.contains(SIZE_MOD_START)) {
443             return buttonSpec;
444         }
445         return buttonSpec.substring(0, buttonSpec.indexOf(SIZE_MOD_START));
446     }
447 
addToDispatchers(View v)448     private void addToDispatchers(View v) {
449         if (mButtonDispatchers != null) {
450             final int indexOfKey = mButtonDispatchers.indexOfKey(v.getId());
451             if (indexOfKey >= 0) {
452                 mButtonDispatchers.valueAt(indexOfKey).addView(v);
453             }
454             if (v instanceof ViewGroup) {
455                 final ViewGroup viewGroup = (ViewGroup)v;
456                 final int N = viewGroup.getChildCount();
457                 for (int i = 0; i < N; i++) {
458                     addToDispatchers(viewGroup.getChildAt(i));
459                 }
460             }
461         }
462     }
463 
464 
465 
clearViews()466     private void clearViews() {
467         if (mButtonDispatchers != null) {
468             for (int i = 0; i < mButtonDispatchers.size(); i++) {
469                 mButtonDispatchers.valueAt(i).clear();
470             }
471         }
472         clearAllChildren(mRot0.findViewById(R.id.nav_buttons));
473         clearAllChildren(mRot90.findViewById(R.id.nav_buttons));
474     }
475 
clearAllChildren(ViewGroup group)476     private void clearAllChildren(ViewGroup group) {
477         for (int i = 0; i < group.getChildCount(); i++) {
478             ((ViewGroup) group.getChildAt(i)).removeAllViews();
479         }
480     }
481 
482     @Override
onPluginConnected(NavBarButtonProvider plugin, Context context)483     public void onPluginConnected(NavBarButtonProvider plugin, Context context) {
484         mPlugins.add(plugin);
485         clearViews();
486         inflateLayout(mCurrentLayout);
487     }
488 
489     @Override
onPluginDisconnected(NavBarButtonProvider plugin)490     public void onPluginDisconnected(NavBarButtonProvider plugin) {
491         mPlugins.remove(plugin);
492         clearViews();
493         inflateLayout(mCurrentLayout);
494     }
495 }
496