NavBarTuner.java revision 46a196e2506797e04b44944f5c2a6599276f770a
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.tuner;
16
17import com.android.systemui.R;
18
19import android.annotation.Nullable;
20import android.app.AlertDialog;
21import android.app.Fragment;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.content.res.ColorStateList;
25import android.content.res.TypedArray;
26import android.graphics.Canvas;
27import android.graphics.drawable.Drawable;
28import android.os.Bundle;
29import android.provider.Settings;
30import android.support.v7.widget.LinearLayoutManager;
31import android.support.v7.widget.RecyclerView;
32import android.support.v7.widget.helper.ItemTouchHelper;
33import android.util.TypedValue;
34import android.view.LayoutInflater;
35import android.view.Menu;
36import android.view.MenuInflater;
37import android.view.MenuItem;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.ImageView;
42import android.widget.SeekBar;
43import android.widget.TextView;
44
45import java.util.ArrayList;
46import java.util.List;
47
48import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.SIZE_MOD_END;
49import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.SIZE_MOD_START;
50import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.extractButton;
51import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.extractSize;
52import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.BACK;
53import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.BUTTON_SEPARATOR;
54import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.GRAVITY_SEPARATOR;
55import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.HOME;
56import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.MENU_IME;
57import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAVSPACE;
58import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAV_BAR_VIEWS;
59import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.RECENT;
60
61public class NavBarTuner extends Fragment implements TunerService.Tunable {
62
63    private static final int SAVE = Menu.FIRST + 1;
64    private static final int RESET = Menu.FIRST + 2;
65
66    private NavBarAdapter mNavBarAdapter;
67
68    @Override
69    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
70            Bundle savedInstanceState) {
71        return new RecyclerView(getContext());
72    }
73
74    @Override
75    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
76        super.onViewCreated(view, savedInstanceState);
77        RecyclerView recyclerView = (RecyclerView) view;
78        final Context context = getContext();
79        recyclerView.setLayoutManager(new LinearLayoutManager(context));
80        mNavBarAdapter = new NavBarAdapter(context);
81        recyclerView.setAdapter(mNavBarAdapter);
82        recyclerView.addItemDecoration(new Dividers(context));
83        final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mNavBarAdapter.mCallbacks);
84        mNavBarAdapter.setTouchHelper(itemTouchHelper);
85        itemTouchHelper.attachToRecyclerView(recyclerView);
86
87        TunerService.get(getContext()).addTunable(this, NAV_BAR_VIEWS);
88    }
89
90    @Override
91    public void onDestroyView() {
92        super.onDestroyView();
93        TunerService.get(getContext()).removeTunable(this);
94    }
95
96    @Override
97    public void onTuningChanged(String key, String navLayout) {
98        if (!NAV_BAR_VIEWS.equals(key)) return;
99        Context context = getContext();
100        if (navLayout == null) {
101            navLayout = context.getString(R.string.config_navBarLayout);
102        }
103        String[] views = navLayout.split(GRAVITY_SEPARATOR);
104        String[] groups = new String[] { NavBarAdapter.START, NavBarAdapter.CENTER,
105                NavBarAdapter.END};
106        CharSequence[] groupLabels = new String[] { getString(R.string.start),
107                getString(R.string.center), getString(R.string.end) };
108        mNavBarAdapter.clear();
109        for (int i = 0; i < 3; i++) {
110            mNavBarAdapter.addButton(groups[i], groupLabels[i]);
111            for (String button : views[i].split(BUTTON_SEPARATOR)) {
112                mNavBarAdapter.addButton(button, getLabel(button, context));
113            }
114        }
115        mNavBarAdapter.addButton(NavBarAdapter.ADD, getString(R.string.add_button));
116        setHasOptionsMenu(true);
117    }
118
119    @Override
120    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
121        super.onCreateOptionsMenu(menu, inflater);
122        // TODO: Show save button conditionally, only when there are changes.
123        menu.add(Menu.NONE, SAVE, Menu.NONE, getString(R.string.save))
124                .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
125        menu.add(Menu.NONE, RESET, Menu.NONE, getString(R.string.reset));
126    }
127
128    @Override
129    public boolean onOptionsItemSelected(MenuItem item) {
130        if (item.getItemId() == SAVE) {
131            if (!mNavBarAdapter.hasHomeButton()) {
132                new AlertDialog.Builder(getContext())
133                        .setTitle(R.string.no_home_title)
134                        .setMessage(R.string.no_home_message)
135                        .setPositiveButton(android.R.string.ok, null)
136                        .show();
137            } else {
138                Settings.Secure.putString(getContext().getContentResolver(),
139                        NAV_BAR_VIEWS, mNavBarAdapter.getNavString());
140            }
141            return true;
142        } else if (item.getItemId() == RESET) {
143            Settings.Secure.putString(getContext().getContentResolver(),
144                    NAV_BAR_VIEWS, null);
145            return true;
146        }
147        return super.onOptionsItemSelected(item);
148    }
149
150    private static CharSequence getLabel(String button, Context context) {
151        if (button.startsWith(HOME)) {
152            return context.getString(R.string.accessibility_home);
153        } else if (button.startsWith(BACK)) {
154            return context.getString(R.string.accessibility_back);
155        } else if (button.startsWith(RECENT)) {
156            return context.getString(R.string.accessibility_recent);
157        } else if (button.startsWith(NAVSPACE)) {
158            return context.getString(R.string.space);
159        } else if (button.startsWith(MENU_IME)) {
160            return context.getString(R.string.menu_ime);
161        }
162        return button;
163    }
164
165    private static class Holder extends RecyclerView.ViewHolder {
166        private TextView title;
167
168        public Holder(View itemView) {
169            super(itemView);
170            title = (TextView) itemView.findViewById(android.R.id.title);
171        }
172    }
173
174    private static class Dividers extends RecyclerView.ItemDecoration {
175        private final Drawable mDivider;
176
177        public Dividers(Context context) {
178            TypedValue value = new TypedValue();
179            context.getTheme().resolveAttribute(android.R.attr.listDivider, value, true);
180            mDivider = context.getDrawable(value.resourceId);
181        }
182
183        @Override
184        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
185            super.onDraw(c, parent, state);
186            final int left = parent.getPaddingLeft();
187            final int right = parent.getWidth() - parent.getPaddingRight();
188
189            final int childCount = parent.getChildCount();
190            for (int i = 0; i < childCount; i++) {
191                final View child = parent.getChildAt(i);
192                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
193                        .getLayoutParams();
194                final int top = child.getBottom() + params.bottomMargin;
195                final int bottom = top + mDivider.getIntrinsicHeight();
196                mDivider.setBounds(left, top, right, bottom);
197                mDivider.draw(c);
198            }
199        }
200    }
201
202    private static class NavBarAdapter extends RecyclerView.Adapter<Holder>
203            implements View.OnClickListener {
204
205        private static final String START = "start";
206        private static final String CENTER = "center";
207        private static final String END = "end";
208        private static final String ADD = "add";
209
210        private static final int ADD_ID = 0;
211        private static final int BUTTON_ID = 1;
212        private static final int CATEGORY_ID = 2;
213
214        private List<String> mButtons = new ArrayList<>();
215        private List<CharSequence> mLabels = new ArrayList<>();
216        private int mCategoryLayout;
217        private int mButtonLayout;
218        private ItemTouchHelper mTouchHelper;
219
220        public NavBarAdapter(Context context) {
221            TypedArray attrs = context.getTheme().obtainStyledAttributes(null,
222                    android.R.styleable.Preference, android.R.attr.preferenceStyle, 0);
223            mButtonLayout = attrs.getResourceId(android.R.styleable.Preference_layout, 0);
224            attrs = context.getTheme().obtainStyledAttributes(null,
225                    android.R.styleable.Preference, android.R.attr.preferenceCategoryStyle, 0);
226            mCategoryLayout = attrs.getResourceId(android.R.styleable.Preference_layout, 0);
227        }
228
229        public void setTouchHelper(ItemTouchHelper itemTouchHelper) {
230            mTouchHelper = itemTouchHelper;
231        }
232
233        public void clear() {
234            mButtons.clear();
235            mLabels.clear();
236            notifyDataSetChanged();
237        }
238
239        public void addButton(String button, CharSequence label) {
240            mButtons.add(button);
241            mLabels.add(label);
242            notifyItemInserted(mLabels.size() - 1);
243        }
244
245        public boolean hasHomeButton() {
246            final int N = mButtons.size();
247            for (int i = 0; i < N; i++) {
248                if (mButtons.get(i).startsWith(HOME)) {
249                    return true;
250                }
251            }
252            return false;
253        }
254
255        public String getNavString() {
256            StringBuilder builder = new StringBuilder();
257            for (int i = 1; i < mButtons.size() - 1; i++) {
258                String button = mButtons.get(i);
259                if (button.equals(CENTER) || button.equals(END)) {
260                    if (builder.length() == 0 || builder.toString().endsWith(GRAVITY_SEPARATOR)) {
261                        // No start or center buttons, fill with a space.
262                        builder.append(NAVSPACE);
263                    }
264                    builder.append(GRAVITY_SEPARATOR);
265                    continue;
266                } else if (builder.length() != 0 && !builder.toString().endsWith(
267                        GRAVITY_SEPARATOR)) {
268                    builder.append(BUTTON_SEPARATOR);
269                }
270                builder.append(button);
271            }
272            if (builder.toString().endsWith(GRAVITY_SEPARATOR)) {
273                // No end buttons, fill with space.
274                builder.append(NAVSPACE);
275            }
276            return builder.toString();
277        }
278
279        @Override
280        public int getItemViewType(int position) {
281            String button = mButtons.get(position);
282            if (button.equals(START) || button.equals(CENTER) || button.equals(END)) {
283                return CATEGORY_ID;
284            }
285            if (button.equals(ADD)) {
286                return ADD_ID;
287            }
288            return BUTTON_ID;
289        }
290
291        @Override
292        public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
293            final Context context = parent.getContext();
294            final LayoutInflater inflater = LayoutInflater.from(context);
295            final View view = inflater.inflate(getLayoutId(viewType), parent, false);
296            if (viewType == BUTTON_ID) {
297                inflater.inflate(R.layout.nav_control_widget,
298                        (ViewGroup) view.findViewById(android.R.id.widget_frame));
299            }
300            return new Holder(view);
301        }
302
303        private int getLayoutId(int viewType) {
304            if (viewType == CATEGORY_ID) {
305                return mCategoryLayout;
306            }
307            return mButtonLayout;
308        }
309
310        @Override
311        public void onBindViewHolder(Holder holder, int position) {
312            holder.title.setText(mLabels.get(position));
313            if (holder.getItemViewType() == BUTTON_ID) {
314                bindButton(holder, position);
315            } else if (holder.getItemViewType() == ADD_ID) {
316                bindAdd(holder);
317            }
318        }
319
320        private void bindAdd(Holder holder) {
321            TypedValue value = new TypedValue();
322            final Context context = holder.itemView.getContext();
323            context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true);
324            final ImageView icon = (ImageView) holder.itemView.findViewById(android.R.id.icon);
325            icon.setImageResource(R.drawable.ic_add);
326            icon.setImageTintList(ColorStateList.valueOf(context.getColor(value.resourceId)));
327            holder.itemView.findViewById(android.R.id.summary).setVisibility(View.GONE);
328            holder.itemView.setClickable(true);
329            holder.itemView.setOnClickListener(new View.OnClickListener() {
330                @Override
331                public void onClick(View v) {
332                    showAddDialog(v.getContext());
333                }
334            });
335        }
336
337        private void bindButton(final Holder holder, int position) {
338            holder.itemView.findViewById(android.R.id.icon_frame).setVisibility(View.GONE);
339            holder.itemView.findViewById(android.R.id.summary).setVisibility(View.GONE);
340            bindClick(holder.itemView.findViewById(R.id.close), holder);
341            bindClick(holder.itemView.findViewById(R.id.width), holder);
342            holder.itemView.findViewById(R.id.drag).setOnTouchListener(new View.OnTouchListener() {
343                @Override
344                public boolean onTouch(View v, MotionEvent event) {
345                    mTouchHelper.startDrag(holder);
346                    return true;
347                }
348            });
349        }
350
351        private void showAddDialog(Context context) {
352            final String[] options = new String[] {
353                    BACK, HOME, RECENT, MENU_IME, NAVSPACE,
354            };
355            final CharSequence[] labels = new CharSequence[options.length];
356            for (int i = 0; i < options.length; i++) {
357                labels[i] = getLabel(options[i], context);
358            }
359            new AlertDialog.Builder(context)
360                    .setTitle(R.string.select_button)
361                    .setItems(labels, new DialogInterface.OnClickListener() {
362                        @Override
363                        public void onClick(DialogInterface dialog, int which) {
364                            int index = mButtons.size() - 1;
365                            mButtons.add(index, options[which]);
366                            mLabels.add(index, labels[which]);
367                            notifyItemInserted(index);
368                        }
369                    }).setNegativeButton(android.R.string.cancel, null)
370                    .show();
371        }
372
373        private void bindClick(View view, Holder holder) {
374            view.setOnClickListener(this);
375            view.setTag(holder);
376        }
377
378        @Override
379        public void onClick(View v) {
380            Holder holder = (Holder) v.getTag();
381            if (v.getId() == R.id.width) {
382                showWidthDialog(holder, v.getContext());
383            } else if (v.getId() == R.id.close) {
384                int position = holder.getAdapterPosition();
385                mButtons.remove(position);
386                mLabels.remove(position);
387                notifyItemRemoved(position);
388            }
389        }
390
391        private void showWidthDialog(final Holder holder, Context context) {
392            final String buttonSpec = mButtons.get(holder.getAdapterPosition());
393            float amount = extractSize(buttonSpec);
394            final AlertDialog dialog = new AlertDialog.Builder(context)
395                    .setTitle(R.string.adjust_button_width)
396                    .setView(R.layout.nav_width_view)
397                    .setNegativeButton(android.R.string.cancel, null).create();
398            dialog.setButton(DialogInterface.BUTTON_POSITIVE,
399                    context.getString(android.R.string.ok),
400                    new DialogInterface.OnClickListener() {
401                        @Override
402                        public void onClick(DialogInterface d, int which) {
403                            final String button = extractButton(buttonSpec);
404                            SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekbar);
405                            if (seekBar.getProgress() == 75) {
406                                mButtons.set(holder.getAdapterPosition(), button);
407                            } else {
408                                float amount = (seekBar.getProgress() + 25) / 100f;
409                                mButtons.set(holder.getAdapterPosition(), button
410                                        + SIZE_MOD_START + amount + SIZE_MOD_END);
411                            }
412                        }
413                    });
414            dialog.show();
415            SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekbar);
416            // Range is .25 - 1.75.
417            seekBar.setMax(150);
418            seekBar.setProgress((int) ((amount - .25f) * 100));
419        }
420
421        @Override
422        public int getItemCount() {
423            return mButtons.size();
424        }
425
426        private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
427            @Override
428            public boolean isLongPressDragEnabled() {
429                return false;
430            }
431
432            @Override
433            public boolean isItemViewSwipeEnabled() {
434                return false;
435            }
436
437            @Override
438            public int getMovementFlags(RecyclerView recyclerView,
439                    RecyclerView.ViewHolder viewHolder) {
440                if (viewHolder.getItemViewType() != BUTTON_ID) {
441                    return makeMovementFlags(0, 0);
442                }
443                int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
444                return makeMovementFlags(dragFlags, 0);
445            }
446
447            @Override
448            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
449                    RecyclerView.ViewHolder target) {
450                int from = viewHolder.getAdapterPosition();
451                int to = target.getAdapterPosition();
452                if (to == 0) {
453                    // Can't go above the top.
454                    return false;
455                }
456                move(from, to, mButtons);
457                move(from, to, mLabels);
458                notifyItemMoved(from, to);
459                return true;
460            }
461
462            private <T> void move(int from, int to, List<T> list) {
463                list.add(from > to ? to : to + 1, list.get(from));
464                list.remove(from > to ? from + 1 : from);
465            }
466
467            @Override
468            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
469                // Don't care.
470            }
471        };
472    }
473}
474