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