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.SparseArray;
23 import android.view.Display;
24 import android.view.Display.Mode;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.view.WindowManager;
29 import android.widget.FrameLayout;
30 import android.widget.LinearLayout;
31 import android.widget.Space;
32 
33 import com.android.systemui.Dependency;
34 import com.android.systemui.R;
35 import com.android.systemui.plugins.PluginListener;
36 import com.android.systemui.plugins.PluginManager;
37 import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider;
38 import com.android.systemui.statusbar.policy.KeyButtonView;
39 import com.android.systemui.tuner.TunerService;
40 import com.android.systemui.tuner.TunerService.Tunable;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Objects;
45 
46 public class NavigationBarInflaterView extends FrameLayout
47         implements Tunable, PluginListener<NavBarButtonProvider> {
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 = "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 KEY = "key";
62     public static final String LEFT = "left";
63     public static final String RIGHT = "right";
64 
65     public static final String GRAVITY_SEPARATOR = ";";
66     public static final String BUTTON_SEPARATOR = ",";
67 
68     public static final String SIZE_MOD_START = "[";
69     public static final String SIZE_MOD_END = "]";
70 
71     public static final String KEY_CODE_START = "(";
72     public static final String KEY_IMAGE_DELIM = ":";
73     public static final String KEY_CODE_END = ")";
74 
75     private final List<NavBarButtonProvider> mPlugins = new ArrayList<>();
76 
77     protected LayoutInflater mLayoutInflater;
78     protected LayoutInflater mLandscapeInflater;
79 
80     protected FrameLayout mRot0;
81     protected FrameLayout mRot90;
82     private boolean isRot0Landscape;
83 
84     private SparseArray<ButtonDispatcher> mButtonDispatchers;
85     private String mCurrentLayout;
86 
87     private View mLastPortrait;
88     private View mLastLandscape;
89 
90     private boolean mAlternativeOrder;
91 
NavigationBarInflaterView(Context context, AttributeSet attrs)92     public NavigationBarInflaterView(Context context, AttributeSet attrs) {
93         super(context, attrs);
94         createInflaters();
95         Display display = ((WindowManager)
96                 context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
97         Mode displayMode = display.getMode();
98         isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
99     }
100 
createInflaters()101     private void createInflaters() {
102         mLayoutInflater = LayoutInflater.from(mContext);
103         Configuration landscape = new Configuration();
104         landscape.setTo(mContext.getResources().getConfiguration());
105         landscape.orientation = Configuration.ORIENTATION_LANDSCAPE;
106         mLandscapeInflater = LayoutInflater.from(mContext.createConfigurationContext(landscape));
107     }
108 
109     @Override
onFinishInflate()110     protected void onFinishInflate() {
111         super.onFinishInflate();
112         inflateChildren();
113         clearViews();
114         inflateLayout(getDefaultLayout());
115     }
116 
inflateChildren()117     private void inflateChildren() {
118         removeAllViews();
119         mRot0 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout, this, false);
120         mRot0.setId(R.id.rot0);
121         addView(mRot0);
122         mRot90 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_rot90, this,
123                 false);
124         mRot90.setId(R.id.rot90);
125         addView(mRot90);
126         updateAlternativeOrder();
127         if (getParent() instanceof NavigationBarView) {
128             ((NavigationBarView) getParent()).updateRotatedViews();
129         }
130     }
131 
getDefaultLayout()132     protected String getDefaultLayout() {
133         return mContext.getString(R.string.config_navBarLayout);
134     }
135 
136     @Override
onAttachedToWindow()137     protected void onAttachedToWindow() {
138         super.onAttachedToWindow();
139         Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
140                 NAV_BAR_RIGHT);
141         Dependency.get(PluginManager.class).addPluginListener(this,
142                 NavBarButtonProvider.class, true /* Allow multiple */);
143     }
144 
145     @Override
onDetachedFromWindow()146     protected void onDetachedFromWindow() {
147         Dependency.get(TunerService.class).removeTunable(this);
148         Dependency.get(PluginManager.class).removePluginListener(this);
149         super.onDetachedFromWindow();
150     }
151 
152     @Override
onTuningChanged(String key, String newValue)153     public void onTuningChanged(String key, String newValue) {
154         if (NAV_BAR_VIEWS.equals(key)) {
155             if (!Objects.equals(mCurrentLayout, newValue)) {
156                 clearViews();
157                 inflateLayout(newValue);
158             }
159         } else if (NAV_BAR_LEFT.equals(key) || NAV_BAR_RIGHT.equals(key)) {
160             clearViews();
161             inflateLayout(mCurrentLayout);
162         }
163     }
164 
setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDisatchers)165     public void setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDisatchers) {
166         mButtonDispatchers = buttonDisatchers;
167         for (int i = 0; i < buttonDisatchers.size(); i++) {
168             initiallyFill(buttonDisatchers.valueAt(i));
169         }
170     }
171 
setAlternativeOrder(boolean alternativeOrder)172     public void setAlternativeOrder(boolean alternativeOrder) {
173         if (alternativeOrder != mAlternativeOrder) {
174             mAlternativeOrder = alternativeOrder;
175             updateAlternativeOrder();
176         }
177     }
178 
updateAlternativeOrder()179     private void updateAlternativeOrder() {
180         updateAlternativeOrder(mRot0.findViewById(R.id.ends_group));
181         updateAlternativeOrder(mRot0.findViewById(R.id.center_group));
182         updateAlternativeOrder(mRot90.findViewById(R.id.ends_group));
183         updateAlternativeOrder(mRot90.findViewById(R.id.center_group));
184     }
185 
updateAlternativeOrder(View v)186     private void updateAlternativeOrder(View v) {
187         if (v instanceof ReverseLinearLayout) {
188             ((ReverseLinearLayout) v).setAlternativeOrder(mAlternativeOrder);
189         }
190     }
191 
initiallyFill(ButtonDispatcher buttonDispatcher)192     private void initiallyFill(ButtonDispatcher buttonDispatcher) {
193         addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.ends_group));
194         addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.center_group));
195         addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.ends_group));
196         addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.center_group));
197     }
198 
addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent)199     private void addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent) {
200         for (int i = 0; i < parent.getChildCount(); i++) {
201             // Need to manually search for each id, just in case each group has more than one
202             // of a single id.  It probably mostly a waste of time, but shouldn't take long
203             // and will only happen once.
204             if (parent.getChildAt(i).getId() == buttonDispatcher.getId()) {
205                 buttonDispatcher.addView(parent.getChildAt(i));
206             } else if (parent.getChildAt(i) instanceof ViewGroup) {
207                 addAll(buttonDispatcher, (ViewGroup) parent.getChildAt(i));
208             }
209         }
210     }
211 
inflateLayout(String newLayout)212     protected void inflateLayout(String newLayout) {
213         mCurrentLayout = newLayout;
214         if (newLayout == null) {
215             newLayout = getDefaultLayout();
216         }
217         String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
218         String[] start = sets[0].split(BUTTON_SEPARATOR);
219         String[] center = sets[1].split(BUTTON_SEPARATOR);
220         String[] end = sets[2].split(BUTTON_SEPARATOR);
221         // Inflate these in start to end order or accessibility traversal will be messed up.
222         inflateButtons(start, (ViewGroup) mRot0.findViewById(R.id.ends_group), isRot0Landscape);
223         inflateButtons(start, (ViewGroup) mRot90.findViewById(R.id.ends_group), !isRot0Landscape);
224 
225         inflateButtons(center, (ViewGroup) mRot0.findViewById(R.id.center_group), isRot0Landscape);
226         inflateButtons(center, (ViewGroup) mRot90.findViewById(R.id.center_group), !isRot0Landscape);
227 
228         addGravitySpacer((LinearLayout) mRot0.findViewById(R.id.ends_group));
229         addGravitySpacer((LinearLayout) mRot90.findViewById(R.id.ends_group));
230 
231         inflateButtons(end, (ViewGroup) mRot0.findViewById(R.id.ends_group), isRot0Landscape);
232         inflateButtons(end, (ViewGroup) mRot90.findViewById(R.id.ends_group), !isRot0Landscape);
233     }
234 
addGravitySpacer(LinearLayout layout)235     private void addGravitySpacer(LinearLayout layout) {
236         layout.addView(new Space(mContext), new LinearLayout.LayoutParams(0, 0, 1));
237     }
238 
inflateButtons(String[] buttons, ViewGroup parent, boolean landscape)239     private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape) {
240         for (int i = 0; i < buttons.length; i++) {
241             inflateButton(buttons[i], parent, landscape);
242         }
243     }
244 
copy(ViewGroup.LayoutParams layoutParams)245     private ViewGroup.LayoutParams copy(ViewGroup.LayoutParams layoutParams) {
246         if (layoutParams instanceof LinearLayout.LayoutParams) {
247             return new LinearLayout.LayoutParams(layoutParams.width, layoutParams.height,
248                     ((LinearLayout.LayoutParams) layoutParams).weight);
249         }
250         return new LayoutParams(layoutParams.width, layoutParams.height);
251     }
252 
253     @Nullable
inflateButton(String buttonSpec, ViewGroup parent, boolean landscape)254     protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape) {
255         LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
256         float size = extractSize(buttonSpec);
257         View v = createView(buttonSpec, parent, inflater, landscape);
258         if (v == null) return null;
259 
260         if (size != 0) {
261             ViewGroup.LayoutParams params = v.getLayoutParams();
262             params.width = (int) (params.width * size);
263         }
264         parent.addView(v);
265         addToDispatchers(v);
266         View lastView = landscape ? mLastLandscape : mLastPortrait;
267         if (lastView != null) {
268             v.setAccessibilityTraversalAfter(lastView.getId());
269         }
270         if (landscape) {
271             mLastLandscape = v;
272         } else {
273             mLastPortrait = v;
274         }
275         return v;
276     }
277 
createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater, boolean landscape)278     private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater,
279             boolean landscape) {
280         View v = null;
281         String button = extractButton(buttonSpec);
282         if (LEFT.equals(button)) {
283             buttonSpec = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, NAVSPACE);
284             button = extractButton(buttonSpec);
285         } else if (RIGHT.equals(button)) {
286             buttonSpec = Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT, MENU_IME);
287             button = extractButton(buttonSpec);
288         }
289         // Let plugins go first so they can override a standard view if they want.
290         for (NavBarButtonProvider provider : mPlugins) {
291             v = provider.createView(buttonSpec, parent);
292             if (v != null) return v;
293         }
294         if (HOME.equals(button)) {
295             v = inflater.inflate(R.layout.home, parent, false);
296         } else if (BACK.equals(button)) {
297             v = inflater.inflate(R.layout.back, parent, false);
298         } else if (RECENT.equals(button)) {
299             v = inflater.inflate(R.layout.recent_apps, parent, false);
300         } else if (MENU_IME.equals(button)) {
301             v = inflater.inflate(R.layout.menu_ime, parent, false);
302         } else if (NAVSPACE.equals(button)) {
303             v = inflater.inflate(R.layout.nav_key_space, parent, false);
304         } else if (CLIPBOARD.equals(button)) {
305             v = inflater.inflate(R.layout.clipboard, parent, false);
306         } else if (button.startsWith(KEY)) {
307             String uri = extractImage(button);
308             int code = extractKeycode(button);
309             v = inflater.inflate(R.layout.custom_key, parent, false);
310             ((KeyButtonView) v).setCode(code);
311             if (uri != null) {
312                 if (uri.contains(":")) {
313                     ((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri));
314                 } else if (uri.contains("/")) {
315                     int index = uri.indexOf('/');
316                     String pkg = uri.substring(0, index);
317                     int id = Integer.parseInt(uri.substring(index + 1));
318                     ((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id));
319                 }
320             }
321         }
322         return v;
323     }
324 
extractImage(String buttonSpec)325     public static String extractImage(String buttonSpec) {
326         if (!buttonSpec.contains(KEY_IMAGE_DELIM)) {
327             return null;
328         }
329         final int start = buttonSpec.indexOf(KEY_IMAGE_DELIM);
330         String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_CODE_END));
331         return subStr;
332     }
333 
extractKeycode(String buttonSpec)334     public static int extractKeycode(String buttonSpec) {
335         if (!buttonSpec.contains(KEY_CODE_START)) {
336             return 1;
337         }
338         final int start = buttonSpec.indexOf(KEY_CODE_START);
339         String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_IMAGE_DELIM));
340         return Integer.parseInt(subStr);
341     }
342 
extractSize(String buttonSpec)343     public static float extractSize(String buttonSpec) {
344         if (!buttonSpec.contains(SIZE_MOD_START)) {
345             return 1;
346         }
347         final int sizeStart = buttonSpec.indexOf(SIZE_MOD_START);
348         String sizeStr = buttonSpec.substring(sizeStart + 1, buttonSpec.indexOf(SIZE_MOD_END));
349         return Float.parseFloat(sizeStr);
350     }
351 
extractButton(String buttonSpec)352     public static String extractButton(String buttonSpec) {
353         if (!buttonSpec.contains(SIZE_MOD_START)) {
354             return buttonSpec;
355         }
356         return buttonSpec.substring(0, buttonSpec.indexOf(SIZE_MOD_START));
357     }
358 
addToDispatchers(View v)359     private void addToDispatchers(View v) {
360         if (mButtonDispatchers != null) {
361             final int indexOfKey = mButtonDispatchers.indexOfKey(v.getId());
362             if (indexOfKey >= 0) {
363                 mButtonDispatchers.valueAt(indexOfKey).addView(v);
364             } else if (v instanceof ViewGroup) {
365                 final ViewGroup viewGroup = (ViewGroup)v;
366                 final int N = viewGroup.getChildCount();
367                 for (int i = 0; i < N; i++) {
368                     addToDispatchers(viewGroup.getChildAt(i));
369                 }
370             }
371         }
372     }
373 
374 
375 
clearViews()376     private void clearViews() {
377         if (mButtonDispatchers != null) {
378             for (int i = 0; i < mButtonDispatchers.size(); i++) {
379                 mButtonDispatchers.valueAt(i).clear();
380             }
381         }
382         clearAllChildren((ViewGroup) mRot0.findViewById(R.id.nav_buttons));
383         clearAllChildren((ViewGroup) mRot90.findViewById(R.id.nav_buttons));
384     }
385 
clearAllChildren(ViewGroup group)386     private void clearAllChildren(ViewGroup group) {
387         for (int i = 0; i < group.getChildCount(); i++) {
388             ((ViewGroup) group.getChildAt(i)).removeAllViews();
389         }
390     }
391 
392     @Override
onPluginConnected(NavBarButtonProvider plugin, Context context)393     public void onPluginConnected(NavBarButtonProvider plugin, Context context) {
394         mPlugins.add(plugin);
395         clearViews();
396         inflateLayout(mCurrentLayout);
397     }
398 
399     @Override
onPluginDisconnected(NavBarButtonProvider plugin)400     public void onPluginDisconnected(NavBarButtonProvider plugin) {
401         mPlugins.remove(plugin);
402         clearViews();
403         inflateLayout(mCurrentLayout);
404     }
405 }
406