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