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
15package com.android.systemui.statusbar.phone;
16
17import android.annotation.Nullable;
18import android.content.Context;
19import android.content.res.Configuration;
20import android.graphics.drawable.Icon;
21import android.util.AttributeSet;
22import android.util.Log;
23import android.util.SparseArray;
24import android.view.Display;
25import android.view.Display.Mode;
26import android.view.Gravity;
27import android.view.LayoutInflater;
28import android.view.Surface;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.WindowManager;
32import android.widget.FrameLayout;
33import android.widget.LinearLayout;
34import android.widget.Space;
35
36import com.android.systemui.Dependency;
37import com.android.systemui.OverviewProxyService;
38import com.android.systemui.R;
39import com.android.systemui.plugins.PluginListener;
40import com.android.systemui.plugins.PluginManager;
41import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider;
42import com.android.systemui.statusbar.phone.ReverseLinearLayout.ReverseRelativeLayout;
43import com.android.systemui.statusbar.policy.KeyButtonView;
44import com.android.systemui.tuner.TunerService;
45import com.android.systemui.tuner.TunerService.Tunable;
46
47import java.util.ArrayList;
48import java.util.List;
49import java.util.Objects;
50
51import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
52
53public 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
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
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
125    protected void onFinishInflate() {
126        super.onFinishInflate();
127        inflateChildren();
128        clearViews();
129        inflateLayout(getDefaultLayout());
130    }
131
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
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
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
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
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
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
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
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
213    public void setAlternativeOrder(boolean alternativeOrder) {
214        if (alternativeOrder != mAlternativeOrder) {
215            mAlternativeOrder = alternativeOrder;
216            updateAlternativeOrder();
217        }
218    }
219
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
227    private void updateAlternativeOrder(View v) {
228        if (v instanceof ReverseLinearLayout) {
229            ((ReverseLinearLayout) v).setAlternativeOrder(mAlternativeOrder);
230        }
231    }
232
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
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
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
284    private void addGravitySpacer(LinearLayout layout) {
285        layout.addView(new Space(mContext), new LinearLayout.LayoutParams(0, 0, 1));
286    }
287
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
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
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
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
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
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
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
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
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
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
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
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
483    public void onPluginConnected(NavBarButtonProvider plugin, Context context) {
484        mPlugins.add(plugin);
485        clearViews();
486        inflateLayout(mCurrentLayout);
487    }
488
489    @Override
490    public void onPluginDisconnected(NavBarButtonProvider plugin) {
491        mPlugins.remove(plugin);
492        clearViews();
493        inflateLayout(mCurrentLayout);
494    }
495}
496