NavBarTuner.java revision c0d7058b14c24cd07912f5629c26b39b7b4673d5
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 android.annotation.Nullable;
18import android.app.Activity;
19import android.app.AlertDialog;
20import android.app.Fragment;
21import android.content.Context;
22import android.content.DialogInterface;
23import android.content.Intent;
24import android.content.res.ColorStateList;
25import android.content.res.Configuration;
26import android.content.res.TypedArray;
27import android.graphics.Canvas;
28import android.graphics.drawable.Drawable;
29import android.net.Uri;
30import android.os.Bundle;
31import android.provider.Settings;
32import android.support.v7.widget.LinearLayoutManager;
33import android.support.v7.widget.RecyclerView;
34import android.support.v7.widget.helper.ItemTouchHelper;
35import android.util.TypedValue;
36import android.view.Display;
37import android.view.LayoutInflater;
38import android.view.Menu;
39import android.view.MenuInflater;
40import android.view.MenuItem;
41import android.view.MotionEvent;
42import android.view.Surface;
43import android.view.View;
44import android.view.ViewGroup;
45import android.widget.ImageView;
46import android.widget.SeekBar;
47import android.widget.TextView;
48
49import com.android.systemui.R;
50
51import java.util.ArrayList;
52import java.util.List;
53
54import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.BACK;
55import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.BUTTON_SEPARATOR;
56import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.CLIPBOARD;
57import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.GRAVITY_SEPARATOR;
58import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.HOME;
59import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY;
60import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_CODE_END;
61import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_CODE_START;
62import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_IMAGE_DELIM;
63import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.MENU_IME;
64import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAVSPACE;
65import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAV_BAR_VIEWS;
66import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.RECENT;
67import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.SIZE_MOD_END;
68import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.SIZE_MOD_START;
69import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.extractButton;
70import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.extractSize;
71
72public class NavBarTuner extends Fragment implements TunerService.Tunable {
73
74    private static final int SAVE = Menu.FIRST + 1;
75    private static final int RESET = Menu.FIRST + 2;
76    private static final int READ_REQUEST = 42;
77
78    private static final float PREVIEW_SCALE = .95f;
79    private static final float PREVIEW_SCALE_LANDSCAPE = .75f;
80
81    private NavBarAdapter mNavBarAdapter;
82    private PreviewNavInflater mPreview;
83
84    @Override
85    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
86            Bundle savedInstanceState) {
87        final View view = inflater.inflate(R.layout.nav_bar_tuner, container, false);
88        inflatePreview((ViewGroup) view.findViewById(R.id.nav_preview_frame));
89        return view;
90    }
91
92    private void inflatePreview(ViewGroup view) {
93        Display display = getActivity().getWindowManager().getDefaultDisplay();
94        boolean isRotated = display.getRotation() == Surface.ROTATION_90
95                || display.getRotation() == Surface.ROTATION_270;
96
97        Configuration config = new Configuration(getContext().getResources().getConfiguration());
98        boolean isPhoneLandscape = isRotated && (config.smallestScreenWidthDp < 600);
99        final float scale = isPhoneLandscape ? PREVIEW_SCALE_LANDSCAPE : PREVIEW_SCALE;
100        config.densityDpi = (int) (config.densityDpi * scale);
101
102        mPreview = (PreviewNavInflater) LayoutInflater.from(getContext().createConfigurationContext(
103                config)).inflate(R.layout.nav_bar_tuner_inflater, view, false);
104        final ViewGroup.LayoutParams layoutParams = mPreview.getLayoutParams();
105        layoutParams.width = (int) ((isPhoneLandscape ? display.getHeight() : display.getWidth())
106                * scale);
107        // Not sure why, but the height dimen is not being scaled with the dp, set it manually
108        // for now.
109        layoutParams.height = (int) (layoutParams.height * scale);
110        if (isPhoneLandscape) {
111            int width = layoutParams.width;
112            layoutParams.width = layoutParams.height;
113            layoutParams.height = width;
114        }
115        view.addView(mPreview);
116
117        if (isRotated) {
118            mPreview.findViewById(R.id.rot0).setVisibility(View.GONE);
119            final View rot90 = mPreview.findViewById(R.id.rot90);
120            rot90.findViewById(R.id.ends_group_lightsout).setVisibility(View.GONE);
121            rot90.findViewById(R.id.center_group_lightsout).setVisibility(View.GONE);
122        } else {
123            mPreview.findViewById(R.id.rot90).setVisibility(View.GONE);
124            final View rot0 = mPreview.findViewById(R.id.rot0);
125            rot0.findViewById(R.id.ends_group_lightsout).setVisibility(View.GONE);
126            rot0.findViewById(R.id.center_group_lightsout).setVisibility(View.GONE);
127        }
128    }
129
130    private void notifyChanged() {
131        mPreview.onTuningChanged(NAV_BAR_VIEWS, mNavBarAdapter.getNavString());
132    }
133
134    @Override
135    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
136        super.onViewCreated(view, savedInstanceState);
137        RecyclerView recyclerView = (RecyclerView) view.findViewById(android.R.id.list);
138        final Context context = getContext();
139        recyclerView.setLayoutManager(new LinearLayoutManager(context));
140        mNavBarAdapter = new NavBarAdapter(context);
141        recyclerView.setAdapter(mNavBarAdapter);
142        recyclerView.addItemDecoration(new Dividers(context));
143        final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mNavBarAdapter.mCallbacks);
144        mNavBarAdapter.setTouchHelper(itemTouchHelper);
145        itemTouchHelper.attachToRecyclerView(recyclerView);
146
147        TunerService.get(getContext()).addTunable(this, NAV_BAR_VIEWS);
148    }
149
150    @Override
151    public void onDestroyView() {
152        super.onDestroyView();
153        TunerService.get(getContext()).removeTunable(this);
154    }
155
156    @Override
157    public void onTuningChanged(String key, String navLayout) {
158        if (!NAV_BAR_VIEWS.equals(key)) return;
159        Context context = getContext();
160        if (navLayout == null) {
161            navLayout = context.getString(R.string.config_navBarLayout);
162        }
163        String[] views = navLayout.split(GRAVITY_SEPARATOR);
164        String[] groups = new String[] { NavBarAdapter.START, NavBarAdapter.CENTER,
165                NavBarAdapter.END};
166        CharSequence[] groupLabels = new String[] { getString(R.string.start),
167                getString(R.string.center), getString(R.string.end) };
168        mNavBarAdapter.clear();
169        for (int i = 0; i < 3; i++) {
170            mNavBarAdapter.addButton(groups[i], groupLabels[i]);
171            for (String button : views[i].split(BUTTON_SEPARATOR)) {
172                mNavBarAdapter.addButton(button, getLabel(button, context));
173            }
174        }
175        mNavBarAdapter.addButton(NavBarAdapter.ADD, getString(R.string.add_button));
176        setHasOptionsMenu(true);
177    }
178
179    @Override
180    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
181        super.onCreateOptionsMenu(menu, inflater);
182        // TODO: Show save button conditionally, only when there are changes.
183        menu.add(Menu.NONE, SAVE, Menu.NONE, getString(R.string.save))
184                .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
185        menu.add(Menu.NONE, RESET, Menu.NONE, getString(R.string.reset));
186    }
187
188    @Override
189    public boolean onOptionsItemSelected(MenuItem item) {
190        if (item.getItemId() == SAVE) {
191            if (!mNavBarAdapter.hasHomeButton()) {
192                new AlertDialog.Builder(getContext())
193                        .setTitle(R.string.no_home_title)
194                        .setMessage(R.string.no_home_message)
195                        .setPositiveButton(android.R.string.ok, null)
196                        .show();
197            } else {
198                Settings.Secure.putString(getContext().getContentResolver(),
199                        NAV_BAR_VIEWS, mNavBarAdapter.getNavString());
200            }
201            return true;
202        } else if (item.getItemId() == RESET) {
203            Settings.Secure.putString(getContext().getContentResolver(),
204                    NAV_BAR_VIEWS, null);
205            return true;
206        }
207        return super.onOptionsItemSelected(item);
208    }
209
210    private static CharSequence getLabel(String button, Context context) {
211        if (button.startsWith(HOME)) {
212            return context.getString(R.string.accessibility_home);
213        } else if (button.startsWith(BACK)) {
214            return context.getString(R.string.accessibility_back);
215        } else if (button.startsWith(RECENT)) {
216            return context.getString(R.string.accessibility_recent);
217        } else if (button.startsWith(NAVSPACE)) {
218            return context.getString(R.string.space);
219        } else if (button.startsWith(MENU_IME)) {
220            return context.getString(R.string.menu_ime);
221        } else if (button.startsWith(CLIPBOARD)) {
222            return context.getString(R.string.clipboard);
223        } else if (button.startsWith(KEY)) {
224            return context.getString(R.string.keycode);
225        }
226        return button;
227    }
228
229    private static class Holder extends RecyclerView.ViewHolder {
230        private TextView title;
231
232        public Holder(View itemView) {
233            super(itemView);
234            title = (TextView) itemView.findViewById(android.R.id.title);
235        }
236    }
237
238    private static class Dividers extends RecyclerView.ItemDecoration {
239        private final Drawable mDivider;
240
241        public Dividers(Context context) {
242            TypedValue value = new TypedValue();
243            context.getTheme().resolveAttribute(android.R.attr.listDivider, value, true);
244            mDivider = context.getDrawable(value.resourceId);
245        }
246
247        @Override
248        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
249            super.onDraw(c, parent, state);
250            final int left = parent.getPaddingLeft();
251            final int right = parent.getWidth() - parent.getPaddingRight();
252
253            final int childCount = parent.getChildCount();
254            for (int i = 0; i < childCount; i++) {
255                final View child = parent.getChildAt(i);
256                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
257                        .getLayoutParams();
258                final int top = child.getBottom() + params.bottomMargin;
259                final int bottom = top + mDivider.getIntrinsicHeight();
260                mDivider.setBounds(left, top, right, bottom);
261                mDivider.draw(c);
262            }
263        }
264    }
265
266    private void selectImage() {
267        startActivityForResult(KeycodeSelectionHelper.getSelectImageIntent(), READ_REQUEST);
268    }
269
270    @Override
271    public void onActivityResult(int requestCode, int resultCode, Intent data) {
272        if (requestCode == READ_REQUEST && resultCode == Activity.RESULT_OK && data != null) {
273            final Uri uri = data.getData();
274            final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION);
275            getContext().getContentResolver().takePersistableUriPermission(uri, takeFlags);
276            mNavBarAdapter.onImageSelected(uri);
277        } else {
278            super.onActivityResult(requestCode, resultCode, data);
279        }
280    }
281
282    private class NavBarAdapter extends RecyclerView.Adapter<Holder>
283            implements View.OnClickListener {
284
285        private static final String START = "start";
286        private static final String CENTER = "center";
287        private static final String END = "end";
288        private static final String ADD = "add";
289
290        private static final int ADD_ID = 0;
291        private static final int BUTTON_ID = 1;
292        private static final int CATEGORY_ID = 2;
293
294        private List<String> mButtons = new ArrayList<>();
295        private List<CharSequence> mLabels = new ArrayList<>();
296        private int mCategoryLayout;
297        private int mButtonLayout;
298        private ItemTouchHelper mTouchHelper;
299
300        // Stored keycode while we wait for image selection on a KEY.
301        private int mKeycode;
302
303        public NavBarAdapter(Context context) {
304            TypedArray attrs = context.getTheme().obtainStyledAttributes(null,
305                    android.R.styleable.Preference, android.R.attr.preferenceStyle, 0);
306            mButtonLayout = attrs.getResourceId(android.R.styleable.Preference_layout, 0);
307            attrs = context.getTheme().obtainStyledAttributes(null,
308                    android.R.styleable.Preference, android.R.attr.preferenceCategoryStyle, 0);
309            mCategoryLayout = attrs.getResourceId(android.R.styleable.Preference_layout, 0);
310        }
311
312        public void setTouchHelper(ItemTouchHelper itemTouchHelper) {
313            mTouchHelper = itemTouchHelper;
314        }
315
316        public void clear() {
317            mButtons.clear();
318            mLabels.clear();
319            notifyDataSetChanged();
320        }
321
322        public void addButton(String button, CharSequence label) {
323            mButtons.add(button);
324            mLabels.add(label);
325            notifyItemInserted(mLabels.size() - 1);
326            notifyChanged();
327        }
328
329        public boolean hasHomeButton() {
330            final int N = mButtons.size();
331            for (int i = 0; i < N; i++) {
332                if (mButtons.get(i).startsWith(HOME)) {
333                    return true;
334                }
335            }
336            return false;
337        }
338
339        public String getNavString() {
340            StringBuilder builder = new StringBuilder();
341            for (int i = 1; i < mButtons.size() - 1; i++) {
342                String button = mButtons.get(i);
343                if (button.equals(CENTER) || button.equals(END)) {
344                    if (builder.length() == 0 || builder.toString().endsWith(GRAVITY_SEPARATOR)) {
345                        // No start or center buttons, fill with a space.
346                        builder.append(NAVSPACE);
347                    }
348                    builder.append(GRAVITY_SEPARATOR);
349                    continue;
350                } else if (builder.length() != 0 && !builder.toString().endsWith(
351                        GRAVITY_SEPARATOR)) {
352                    builder.append(BUTTON_SEPARATOR);
353                }
354                builder.append(button);
355            }
356            if (builder.toString().endsWith(GRAVITY_SEPARATOR)) {
357                // No end buttons, fill with space.
358                builder.append(NAVSPACE);
359            }
360            return builder.toString();
361        }
362
363        @Override
364        public int getItemViewType(int position) {
365            String button = mButtons.get(position);
366            if (button.equals(START) || button.equals(CENTER) || button.equals(END)) {
367                return CATEGORY_ID;
368            }
369            if (button.equals(ADD)) {
370                return ADD_ID;
371            }
372            return BUTTON_ID;
373        }
374
375        @Override
376        public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
377            final Context context = parent.getContext();
378            final LayoutInflater inflater = LayoutInflater.from(context);
379            final View view = inflater.inflate(getLayoutId(viewType), parent, false);
380            if (viewType == BUTTON_ID) {
381                inflater.inflate(R.layout.nav_control_widget,
382                        (ViewGroup) view.findViewById(android.R.id.widget_frame));
383            }
384            return new Holder(view);
385        }
386
387        private int getLayoutId(int viewType) {
388            if (viewType == CATEGORY_ID) {
389                return mCategoryLayout;
390            }
391            return mButtonLayout;
392        }
393
394        @Override
395        public void onBindViewHolder(Holder holder, int position) {
396            holder.title.setText(mLabels.get(position));
397            if (holder.getItemViewType() == BUTTON_ID) {
398                bindButton(holder, position);
399            } else if (holder.getItemViewType() == ADD_ID) {
400                bindAdd(holder);
401            }
402        }
403
404        private void bindAdd(Holder holder) {
405            TypedValue value = new TypedValue();
406            final Context context = holder.itemView.getContext();
407            context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true);
408            final ImageView icon = (ImageView) holder.itemView.findViewById(android.R.id.icon);
409            icon.setImageResource(R.drawable.ic_add);
410            icon.setImageTintList(ColorStateList.valueOf(context.getColor(value.resourceId)));
411            holder.itemView.findViewById(android.R.id.summary).setVisibility(View.GONE);
412            holder.itemView.setClickable(true);
413            holder.itemView.setOnClickListener(new View.OnClickListener() {
414                @Override
415                public void onClick(View v) {
416                    showAddDialog(v.getContext());
417                }
418            });
419        }
420
421        private void bindButton(final Holder holder, int position) {
422            holder.itemView.findViewById(android.R.id.icon_frame).setVisibility(View.GONE);
423            holder.itemView.findViewById(android.R.id.summary).setVisibility(View.GONE);
424            bindClick(holder.itemView.findViewById(R.id.close), holder);
425            bindClick(holder.itemView.findViewById(R.id.width), holder);
426            holder.itemView.findViewById(R.id.drag).setOnTouchListener(new View.OnTouchListener() {
427                @Override
428                public boolean onTouch(View v, MotionEvent event) {
429                    mTouchHelper.startDrag(holder);
430                    return true;
431                }
432            });
433        }
434
435        private void showAddDialog(final Context context) {
436            final String[] options = new String[] {
437                    BACK, HOME, RECENT, MENU_IME, NAVSPACE, CLIPBOARD, KEY,
438            };
439            final CharSequence[] labels = new CharSequence[options.length];
440            for (int i = 0; i < options.length; i++) {
441                labels[i] = getLabel(options[i], context);
442            }
443            new AlertDialog.Builder(context)
444                    .setTitle(R.string.select_button)
445                    .setItems(labels, new DialogInterface.OnClickListener() {
446                        @Override
447                        public void onClick(DialogInterface dialog, int which) {
448                            if (KEY.equals(options[which])) {
449                                showKeyDialogs(context);
450                            } else {
451                                int index = mButtons.size() - 1;
452                                showAddedMessage(context, options[which]);
453                                mButtons.add(index, options[which]);
454                                mLabels.add(index, labels[which]);
455
456                                notifyItemInserted(index);
457                                notifyChanged();
458                            }
459                        }
460                    }).setNegativeButton(android.R.string.cancel, null)
461                    .show();
462        }
463
464        private void onImageSelected(Uri uri) {
465            int index = mButtons.size() - 1;
466            mButtons.add(index, KEY + KEY_CODE_START + mKeycode + KEY_IMAGE_DELIM + uri.toString()
467                    + KEY_CODE_END);
468            mLabels.add(index, getLabel(KEY, getContext()));
469
470            notifyItemInserted(index);
471            notifyChanged();
472        }
473
474        private void showKeyDialogs(final Context context) {
475            final KeycodeSelectionHelper.OnSelectionComplete listener =
476                    new KeycodeSelectionHelper.OnSelectionComplete() {
477                        @Override
478                        public void onSelectionComplete(int code) {
479                            mKeycode = code;
480                            selectImage();
481                        }
482                    };
483            new AlertDialog.Builder(context)
484                    .setTitle(R.string.keycode)
485                    .setMessage(R.string.keycode_description)
486                    .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
487                        @Override
488                        public void onClick(DialogInterface dialog, int which) {
489                            KeycodeSelectionHelper.showKeycodeSelect(context, listener);
490                        }
491                    }).show();
492        }
493
494        private void showAddedMessage(Context context, String button) {
495            if (CLIPBOARD.equals(button)) {
496                new AlertDialog.Builder(context)
497                        .setTitle(R.string.clipboard)
498                        .setMessage(R.string.clipboard_description)
499                        .setPositiveButton(android.R.string.ok, null)
500                        .show();
501            }
502        }
503
504        private void bindClick(View view, Holder holder) {
505            view.setOnClickListener(this);
506            view.setTag(holder);
507        }
508
509        @Override
510        public void onClick(View v) {
511            Holder holder = (Holder) v.getTag();
512            if (v.getId() == R.id.width) {
513                showWidthDialog(holder, v.getContext());
514            } else if (v.getId() == R.id.close) {
515                int position = holder.getAdapterPosition();
516                mButtons.remove(position);
517                mLabels.remove(position);
518                notifyItemRemoved(position);
519                notifyChanged();
520            }
521        }
522
523        private void showWidthDialog(final Holder holder, Context context) {
524            final String buttonSpec = mButtons.get(holder.getAdapterPosition());
525            float amount = extractSize(buttonSpec);
526            final AlertDialog dialog = new AlertDialog.Builder(context)
527                    .setTitle(R.string.adjust_button_width)
528                    .setView(R.layout.nav_width_view)
529                    .setNegativeButton(android.R.string.cancel, null).create();
530            dialog.setButton(DialogInterface.BUTTON_POSITIVE,
531                    context.getString(android.R.string.ok),
532                    new DialogInterface.OnClickListener() {
533                        @Override
534                        public void onClick(DialogInterface d, int which) {
535                            final String button = extractButton(buttonSpec);
536                            SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekbar);
537                            if (seekBar.getProgress() == 75) {
538                                mButtons.set(holder.getAdapterPosition(), button);
539                            } else {
540                                float amount = (seekBar.getProgress() + 25) / 100f;
541                                mButtons.set(holder.getAdapterPosition(), button
542                                        + SIZE_MOD_START + amount + SIZE_MOD_END);
543                            }
544                            notifyChanged();
545                        }
546                    });
547            dialog.show();
548            SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekbar);
549            // Range is .25 - 1.75.
550            seekBar.setMax(150);
551            seekBar.setProgress((int) ((amount - .25f) * 100));
552        }
553
554        @Override
555        public int getItemCount() {
556            return mButtons.size();
557        }
558
559        private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
560            @Override
561            public boolean isLongPressDragEnabled() {
562                return false;
563            }
564
565            @Override
566            public boolean isItemViewSwipeEnabled() {
567                return false;
568            }
569
570            @Override
571            public int getMovementFlags(RecyclerView recyclerView,
572                    RecyclerView.ViewHolder viewHolder) {
573                if (viewHolder.getItemViewType() != BUTTON_ID) {
574                    return makeMovementFlags(0, 0);
575                }
576                int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
577                return makeMovementFlags(dragFlags, 0);
578            }
579
580            @Override
581            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
582                    RecyclerView.ViewHolder target) {
583                int from = viewHolder.getAdapterPosition();
584                int to = target.getAdapterPosition();
585                if (to == 0) {
586                    // Can't go above the top.
587                    return false;
588                }
589                move(from, to, mButtons);
590                move(from, to, mLabels);
591                notifyChanged();
592                notifyItemMoved(from, to);
593                return true;
594            }
595
596            private <T> void move(int from, int to, List<T> list) {
597                list.add(from > to ? to : to + 1, list.get(from));
598                list.remove(from > to ? from + 1 : from);
599            }
600
601            @Override
602            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
603                // Don't care.
604            }
605        };
606    }
607}
608