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