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