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