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