1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.settings.dialog; 18 19 import android.content.Intent; 20 import android.content.res.Resources; 21 import android.graphics.drawable.Drawable; 22 import android.net.Uri; 23 import android.os.Bundle; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.text.TextUtils; 27 import android.util.Log; 28 29 import java.util.ArrayList; 30 import java.util.List; 31 32 /** 33 * A data class which represents a settings layout within an 34 * {@link SettingsLayoutFragment}. Represents a list of choices the 35 * user can make, a radio-button list of configuration options, or just a 36 * list of information. 37 */ 38 public class Layout implements Parcelable { 39 40 public interface LayoutNodeRefreshListener { onRefreshView()41 void onRefreshView(); getSelectedNode()42 Node getSelectedNode(); 43 } 44 45 public interface ContentNodeRefreshListener { onRefreshView()46 void onRefreshView(); 47 } 48 49 public interface Node { getTitle()50 String getTitle(); 51 } 52 53 private abstract static class LayoutTreeNode implements Node { 54 LayoutTreeBranch mParent; 55 isEnabled()56 /* package */ boolean isEnabled() { 57 return false; 58 } 59 getAppearance()60 /* package */ abstract Appearance getAppearance(); Log(int level)61 /* package */ abstract void Log(int level); 62 } 63 64 private abstract static class LayoutTreeBranch extends LayoutTreeNode { 65 final ArrayList<LayoutTreeNode> mChildren; LayoutTreeBranch()66 LayoutTreeBranch() { 67 mChildren = new ArrayList<>(); 68 } 69 } 70 71 public static class LayoutRow { 72 public static final int NO_CHECK_SET = 0; 73 public static final int VIEW_TYPE_ACTION = 0; 74 public static final int VIEW_TYPE_STATIC = 1; 75 public static final int VIEW_TYPE_WALLOFTEXT = 2; 76 77 private String mTitle; 78 private StringGetter mDescription; 79 private final LayoutTreeNode mNode; 80 private final boolean mEnabled; 81 private final int mViewType; 82 private BooleanGetter mChecked = FALSE_GETTER; 83 private Drawable mIcon = null; 84 private int mSelectionIndex; 85 getNode()86 public Node getNode() { 87 return mNode; 88 } 89 getIconUri()90 public Uri getIconUri() { 91 return null; 92 } 93 getIcon()94 public Drawable getIcon() { 95 return mIcon; 96 } 97 getCheckSetId()98 public int getCheckSetId() { 99 return 0; 100 } 101 isChecked()102 public boolean isChecked() { 103 return mChecked.get(); 104 } 105 getChecked()106 public BooleanGetter getChecked() { 107 return mChecked; 108 } 109 setChecked(boolean v)110 public void setChecked(boolean v) { 111 mChecked = v ? TRUE_GETTER : FALSE_GETTER; 112 } 113 infoOnly()114 public boolean infoOnly() { 115 return false; 116 } 117 isEnabled()118 public boolean isEnabled() { 119 return mEnabled; 120 } 121 hasNext()122 public boolean hasNext() { 123 return false; 124 } 125 getTitle()126 public String getTitle() { 127 return mTitle; 128 } 129 getDescription()130 public StringGetter getDescription() { 131 return mDescription; 132 } 133 getViewType()134 public int getViewType() { 135 return mViewType; 136 } 137 isRadio()138 public boolean isRadio() { 139 return mNode instanceof SelectionGroup; 140 } 141 getRadioId()142 public int getRadioId() { 143 return ((SelectionGroup) mNode).getId(mSelectionIndex); 144 } 145 setRadioSelectedIndex()146 public boolean setRadioSelectedIndex() { 147 return ((SelectionGroup) mNode).setSelectedIndex(mSelectionIndex); 148 } 149 isGoBack()150 public boolean isGoBack() { 151 if (mNode instanceof Action) { 152 Action a = (Action) mNode; 153 if (a.mActionId == Action.ACTION_BACK) { 154 return true; 155 } 156 } 157 return false; 158 } 159 getUserAction()160 public Action getUserAction() { 161 if (mNode instanceof Action) { 162 Action a = (Action) mNode; 163 if (a.mActionId != Action.ACTION_NONE) { 164 return a; 165 } 166 } 167 return null; 168 } 169 getContentIconRes()170 public int getContentIconRes() { 171 if (mNode instanceof Header) { 172 return ((Header) mNode).mContentIconRes; 173 } 174 return 0; 175 } 176 LayoutRow(LayoutTreeNode node)177 public LayoutRow(LayoutTreeNode node) { 178 mNode = node; 179 if (node instanceof Static) { 180 mViewType = VIEW_TYPE_STATIC; 181 mTitle = ((Static) node).mTitle; 182 } else if (node instanceof WallOfText) { 183 mViewType = VIEW_TYPE_WALLOFTEXT; 184 mTitle = ((WallOfText) node).mTitle; 185 } else { 186 mViewType = VIEW_TYPE_ACTION; 187 } 188 mEnabled = node.isEnabled(); 189 final Appearance a = node.getAppearance(); 190 if (a != null) { 191 mTitle = a.getTitle(); 192 mDescription = a.mDescriptionGetter; 193 mIcon = a.getIcon(); 194 mChecked = a.getChecked(); 195 } 196 } 197 LayoutRow(final SelectionGroup selectionGroup, int selectedIndex)198 public LayoutRow(final SelectionGroup selectionGroup, int selectedIndex) { 199 mNode = selectionGroup; 200 mViewType = VIEW_TYPE_ACTION; 201 mSelectionIndex = selectedIndex; 202 mTitle = selectionGroup.getTitle(selectedIndex); 203 mChecked = selectionGroup.getChecked(selectedIndex) ? TRUE_GETTER : FALSE_GETTER; 204 mDescription = selectionGroup.getDescription(selectedIndex); 205 mEnabled = true; 206 } 207 } 208 209 /** 210 * Getter object for boolean values, mostly used for checked state on headers/actions/etc. 211 * This is a slightly simpler alternative to using a LayoutGetter to make sure the checked state 212 * is always up to date. 213 */ 214 public abstract static class BooleanGetter { 215 private ContentNodeRefreshListener mListener; 216 setListener(ContentNodeRefreshListener listener)217 public void setListener(ContentNodeRefreshListener listener) { 218 mListener = listener; 219 } 220 get()221 public abstract boolean get(); 222 223 /** 224 * Notification from client that antecedent data has changed and the string should be 225 * redisplayed. 226 */ refreshView()227 public void refreshView() { 228 if (mListener != null) { 229 mListener.onRefreshView(); 230 } 231 } 232 } 233 234 /** 235 * Basic mutable implementation of BooleanGetter. Call {@link MutableBooleanGetter#set(boolean)} 236 * to modify the state and automatically refresh the view. 237 */ 238 public final static class MutableBooleanGetter extends BooleanGetter { 239 private boolean mState; 240 MutableBooleanGetter()241 public MutableBooleanGetter() { 242 mState = false; 243 } 244 MutableBooleanGetter(boolean state)245 public MutableBooleanGetter(boolean state) { 246 mState = state; 247 } 248 249 @Override get()250 public boolean get() { 251 return mState; 252 } 253 254 /** 255 * Set the boolean value for this object. Automatically calls {@link #refreshView()} 256 * @param newState State to set 257 */ set(boolean newState)258 public void set(boolean newState) { 259 mState = newState; 260 refreshView(); 261 } 262 } 263 264 private final static class LiteralBooleanGetter extends BooleanGetter { 265 private final boolean mState; 266 LiteralBooleanGetter(boolean state)267 public LiteralBooleanGetter(boolean state) { 268 mState = state; 269 } 270 271 @Override get()272 public boolean get() { 273 return mState; 274 } 275 276 @Override setListener(ContentNodeRefreshListener listener)277 public void setListener(ContentNodeRefreshListener listener) {} 278 279 @Override refreshView()280 public void refreshView() {} 281 } 282 283 private static final BooleanGetter TRUE_GETTER = new LiteralBooleanGetter(true); 284 private static final BooleanGetter FALSE_GETTER = new LiteralBooleanGetter(false); 285 286 public abstract static class DrawableGetter { get()287 public abstract Drawable get(); 288 289 /** 290 * Notification from client that antecedent data has changed and the drawable should be 291 * redisplayed. 292 */ refreshView()293 public void refreshView() { 294 //TODO - When implementing, ensure that multiple updates from the same event do not 295 // cause multiple view updates. 296 } 297 } 298 299 public abstract static class StringGetter { 300 private ContentNodeRefreshListener mListener; 301 setListener(ContentNodeRefreshListener listener)302 public void setListener(ContentNodeRefreshListener listener) { 303 mListener = listener; 304 } 305 get()306 public abstract String get(); 307 308 /** 309 * Notification from client that antecedent data has changed and the string should be 310 * redisplayed. 311 */ refreshView()312 public void refreshView() { 313 if (mListener != null) { 314 mListener.onRefreshView(); 315 } 316 } 317 } 318 319 /** 320 * Implementation of "StringGetter" that stores and returns a literal string. 321 */ 322 private static class LiteralStringGetter extends StringGetter { 323 private final String mValue; get()324 public String get() { 325 return mValue; 326 } LiteralStringGetter(String value)327 LiteralStringGetter(String value) { 328 mValue = value; 329 } 330 } 331 332 /** 333 * Implementation of "StringGetter" that stores a string resource id and returns a string. 334 */ 335 private static class ResourceStringGetter extends StringGetter { 336 private final int mStringResourceId; 337 private final Resources mRes; get()338 public String get() { 339 return mRes.getString(mStringResourceId); 340 } ResourceStringGetter(Resources res, int stringResourceId)341 ResourceStringGetter(Resources res, int stringResourceId) { 342 mRes = res; 343 mStringResourceId = stringResourceId; 344 } 345 } 346 347 public abstract static class LayoutGetter extends LayoutTreeNode { 348 // Layout manages this listener; removing it when this node is not visible and setting it 349 // when it is. Users are expected to set the listener with Layout.setRefreshViewListener. 350 private LayoutNodeRefreshListener mListener; 351 setListener(LayoutNodeRefreshListener listener)352 public void setListener(LayoutNodeRefreshListener listener) { 353 mListener = listener; 354 } 355 notVisible()356 public void notVisible() { 357 mListener = null; 358 } 359 get()360 public abstract Layout get(); 361 getSelectedNode()362 public Node getSelectedNode() { 363 if (mListener != null) { 364 return mListener.getSelectedNode(); 365 } else { 366 return null; 367 } 368 } 369 370 /** 371 * Notification from client that antecedent data has changed and the list containing the 372 * contents of this getter should be updated. 373 */ refreshView()374 public void refreshView() { 375 if (mListener != null) { 376 mListener.onRefreshView(); 377 } 378 } 379 380 @Override getTitle()381 public String getTitle() { 382 return null; 383 } 384 385 @Override getAppearance()386 /* package */ Appearance getAppearance() { 387 return null; 388 } 389 390 @Override Log(int level)391 /* package */ void Log(int level) { 392 Log.d("Layout", indent(level) + "LayoutGetter"); 393 Layout l = get(); 394 l.Log(level + 1); 395 } 396 } 397 398 /** 399 * A list of "select one of" radio style buttons. 400 */ 401 public static class SelectionGroup extends LayoutTreeNode { 402 final String mTitle[]; 403 final StringGetter mDescription[]; 404 final int mId[]; 405 int mSelectedIndex; 406 407 public static final class Builder { 408 private static class Item { 409 public final String title; 410 public final StringGetter description; 411 public final int id; 412 Item(String title, String description, int id)413 public Item(String title, String description, int id) { 414 this.title = title; 415 if (TextUtils.isEmpty(description)) { 416 this.description = null; 417 } else { 418 this.description = new LiteralStringGetter(description); 419 } 420 this.id = id; 421 } 422 } 423 424 private final List<Item> mItems; 425 private boolean mSetInitialId = false; 426 private int mInitialId; 427 Builder()428 public Builder() { 429 mItems = new ArrayList<>(); 430 } 431 Builder(int count)432 public Builder(int count) { 433 mItems = new ArrayList<>(count); 434 } 435 add(String title, String description, int id)436 public Builder add(String title, String description, int id) { 437 mItems.add(new Item(title, description, id)); 438 return this; 439 } 440 select(int id)441 public Builder select(int id) { 442 mSetInitialId = true; 443 mInitialId = id; 444 return this; 445 } 446 build()447 public SelectionGroup build() { 448 final int size = mItems.size(); 449 final String[] titles = new String[size]; 450 final StringGetter[] descriptions = new StringGetter[size]; 451 final int[] ids = new int[size]; 452 int i = 0; 453 for (final Item item : mItems) { 454 titles[i] = item.title; 455 descriptions[i] = item.description; 456 ids[i] = item.id; 457 i++; 458 } 459 final SelectionGroup selectionGroup = new SelectionGroup(titles, descriptions, ids); 460 if (mSetInitialId) { 461 selectionGroup.setSelected(mInitialId); 462 } 463 return selectionGroup; 464 } 465 } 466 SelectionGroup(Resources res, int param[][])467 public SelectionGroup(Resources res, int param[][]) { 468 mSelectedIndex = -1; 469 mTitle = new String[param.length]; 470 mDescription = new StringGetter[param.length]; 471 mId = new int[param.length]; 472 for (int i = 0; i < param.length; ++i) { 473 mTitle[i] = res.getString(param[i][0]); 474 mId[i] = param[i][1]; 475 } 476 } 477 SelectionGroup(String[] titles, StringGetter[] descriptions, int[] ids)478 public SelectionGroup(String[] titles, StringGetter[] descriptions, int[] ids) { 479 mSelectedIndex = -1; 480 mTitle = titles; 481 mDescription = descriptions; 482 mId = ids; 483 } 484 485 @Override getTitle()486 public String getTitle() { 487 if (mSelectedIndex >= 0 && mSelectedIndex < mTitle.length) { 488 return mTitle[mSelectedIndex]; 489 } else { 490 return ""; 491 } 492 } 493 494 @Override getAppearance()495 /* package */ Appearance getAppearance() { 496 return null; 497 } 498 499 @Override Log(int level)500 /* package */ void Log(int level) { 501 Log.d("Layout", indent(level) + "SelectionGroup '" + getTitle() + "'"); 502 } 503 getTitle(int index)504 public String getTitle(int index) { 505 return mTitle[index]; 506 } 507 getDescription(int index)508 public StringGetter getDescription(int index) { 509 return mDescription[index]; 510 } 511 getChecked(int index)512 public boolean getChecked(int index) { 513 return mSelectedIndex == index; 514 } 515 size()516 public int size() { 517 return mTitle.length; 518 } 519 setSelected(int id)520 public void setSelected(int id) { 521 mSelectedIndex = -1; 522 for (int index = 0, dim = mId.length; index < dim; ++index) { 523 if (mId[index] == id) { 524 mSelectedIndex = index; 525 break; 526 } 527 } 528 } 529 setSelectedIndex(int selectedIndex)530 public boolean setSelectedIndex(int selectedIndex) { 531 if (mSelectedIndex != selectedIndex && selectedIndex < mId.length) { 532 mSelectedIndex = selectedIndex; 533 return true; 534 } else { 535 return false; 536 } 537 } 538 getId(int index)539 public int getId(int index) { 540 return mId[index]; 541 } 542 getId()543 public int getId() { 544 return mId[mSelectedIndex]; 545 } 546 } 547 548 /** 549 * Implementation of "StringGetter" that returns a string describing the currently selected 550 * item in a SelectionGroup. 551 */ 552 public static class SelectionGroupStringGetter extends StringGetter { 553 554 private final SelectionGroup mSelectionGroup; 555 556 @Override get()557 public String get() { 558 return mSelectionGroup.getTitle(); 559 } 560 SelectionGroupStringGetter(SelectionGroup selectionGroup)561 public SelectionGroupStringGetter(SelectionGroup selectionGroup) { 562 mSelectionGroup = selectionGroup; 563 } 564 565 } 566 567 private static class Appearance { 568 private Drawable mIcon; 569 private DrawableGetter mIconGetter; 570 private String mTitle; 571 private StringGetter mDescriptionGetter; 572 private BooleanGetter mChecked = FALSE_GETTER; 573 toString()574 public String toString() { 575 StringBuilder stringBuilder = new StringBuilder() 576 .append("'") 577 .append(mTitle) 578 .append("'"); 579 if (mDescriptionGetter != null) { 580 stringBuilder 581 .append(" : '") 582 .append(mDescriptionGetter.get()) 583 .append("'"); 584 } 585 stringBuilder 586 .append(" : '") 587 .append(mChecked.get()) 588 .append("'"); 589 return stringBuilder.toString(); 590 } 591 getTitle()592 public String getTitle() { 593 return mTitle; 594 } 595 getIcon()596 public Drawable getIcon() { 597 if (mIconGetter != null) { 598 return mIconGetter.get(); 599 } else { 600 return mIcon; 601 } 602 } 603 getChecked()604 public BooleanGetter getChecked() { 605 return mChecked; 606 } 607 setChecked(boolean checked)608 public void setChecked(boolean checked) { 609 mChecked = checked ? TRUE_GETTER : FALSE_GETTER; 610 } 611 setChecked(BooleanGetter getter)612 public void setChecked(BooleanGetter getter) { 613 mChecked = getter; 614 } 615 } 616 617 /** 618 * Header is a container for a sub-menu of "LayoutTreeNode" items. 619 */ 620 public static class Header extends LayoutTreeBranch { 621 private final Appearance mAppearance = new Appearance(); 622 private int mSelectedIndex = 0; 623 private String mDetailedDescription; 624 private int mContentIconRes = 0; 625 private boolean mEnabled = true; 626 627 public static class Builder { 628 private final Resources mRes; 629 private final Header mHeader = new Header(); 630 Builder(Resources res)631 public Builder(Resources res) { 632 mRes = res; 633 } 634 icon(int resId)635 public Builder icon(int resId) { 636 mHeader.mAppearance.mIcon = mRes.getDrawable(resId); 637 return this; 638 } 639 icon(DrawableGetter drawableGetter)640 public Builder icon(DrawableGetter drawableGetter) { 641 mHeader.mAppearance.mIconGetter = drawableGetter; 642 return this; 643 } 644 contentIconRes(int resId)645 public Builder contentIconRes(int resId) { 646 mHeader.mContentIconRes = resId; 647 return this; 648 } 649 title(int resId)650 public Builder title(int resId) { 651 mHeader.mAppearance.mTitle = mRes.getString(resId); 652 return this; 653 } 654 description(int resId)655 public Builder description(int resId) { 656 mHeader.mAppearance.mDescriptionGetter = new ResourceStringGetter(mRes, resId); 657 return this; 658 } 659 description(SelectionGroup selectionGroup)660 public Builder description(SelectionGroup selectionGroup) { 661 mHeader.mAppearance.mDescriptionGetter = new SelectionGroupStringGetter( 662 selectionGroup); 663 return this; 664 } 665 title(String title)666 public Builder title(String title) { 667 mHeader.mAppearance.mTitle = title; 668 return this; 669 } 670 description(String description)671 public Builder description(String description) { 672 mHeader.mAppearance.mDescriptionGetter = description != null ? 673 new LiteralStringGetter(description) : null; 674 return this; 675 } 676 description(StringGetter description)677 public Builder description(StringGetter description) { 678 mHeader.mAppearance.mDescriptionGetter = description; 679 return this; 680 } 681 detailedDescription(int resId)682 public Builder detailedDescription(int resId) { 683 mHeader.mDetailedDescription = mRes.getString(resId); 684 return this; 685 } 686 detailedDescription(String detailedDescription)687 public Builder detailedDescription(String detailedDescription) { 688 mHeader.mDetailedDescription = detailedDescription; 689 return this; 690 } 691 enabled(boolean enabled)692 public Builder enabled(boolean enabled) { 693 mHeader.mEnabled = enabled; 694 return this; 695 } 696 build()697 public Header build() { 698 return mHeader; 699 } 700 } 701 702 @Override getTitle()703 public String getTitle() { 704 return mAppearance.getTitle(); 705 } 706 add(LayoutTreeNode node)707 public Header add(LayoutTreeNode node) { 708 node.mParent = this; 709 mChildren.add(node); 710 return this; 711 } 712 setSelectionGroup(SelectionGroup selectionGroup)713 public Header setSelectionGroup(SelectionGroup selectionGroup) { 714 selectionGroup.mParent = this; 715 mChildren.add(selectionGroup); 716 mAppearance.mDescriptionGetter = new SelectionGroupStringGetter( 717 selectionGroup); 718 return this; 719 } 720 getDetailedDescription()721 String getDetailedDescription() { 722 return mDetailedDescription; 723 } 724 725 @Override isEnabled()726 /* package */ boolean isEnabled() { 727 return mEnabled; 728 } 729 730 @Override getAppearance()731 /* package */ Appearance getAppearance() { 732 return mAppearance; 733 } 734 735 @Override Log(int level)736 /* package */ void Log(int level) { 737 Log.d("Layout", indent(level) + "Header " + mAppearance); 738 for (LayoutTreeNode i : mChildren) 739 i.Log(level + 1); 740 } 741 } 742 743 public static class Action extends LayoutTreeNode { 744 public static final int ACTION_NONE = -1; 745 public static final int ACTION_INTENT = -2; 746 public static final int ACTION_BACK = -3; 747 private final int mActionId; 748 private final Intent mIntent; 749 private final Appearance mAppearance = new Appearance(); 750 private Bundle mActionData; 751 private boolean mDefaultSelection = false; 752 private boolean mEnabled = true; 753 Action(int id)754 public Action(int id) { 755 mActionId = id; 756 mIntent = null; 757 } 758 Action(Intent intent)759 private Action(Intent intent) { 760 mActionId = ACTION_INTENT; 761 mIntent = intent; 762 } 763 764 public static class Builder { 765 private final Resources mRes; 766 private final Action mAction; 767 Builder(Resources res, int id)768 public Builder(Resources res, int id) { 769 mRes = res; 770 mAction = new Action(id); 771 } 772 Builder(Resources res, Intent intent)773 public Builder(Resources res, Intent intent) { 774 mRes = res; 775 mAction = new Action(intent); 776 } 777 title(int resId)778 public Builder title(int resId) { 779 mAction.mAppearance.mTitle = mRes.getString(resId); 780 return this; 781 } 782 description(int resId)783 public Builder description(int resId) { 784 mAction.mAppearance.mDescriptionGetter = new LiteralStringGetter(mRes.getString( 785 resId)); 786 return this; 787 } 788 title(String title)789 public Builder title(String title) { 790 mAction.mAppearance.mTitle = title; 791 return this; 792 } 793 icon(int resId)794 public Builder icon(int resId) { 795 mAction.mAppearance.mIcon = mRes.getDrawable(resId); 796 return this; 797 } 798 icon(Drawable drawable)799 public Builder icon(Drawable drawable) { 800 mAction.mAppearance.mIcon = drawable; 801 return this; 802 } 803 description(String description)804 public Builder description(String description) { 805 mAction.mAppearance.mDescriptionGetter = new LiteralStringGetter(description); 806 return this; 807 } 808 description(StringGetter description)809 public Builder description(StringGetter description) { 810 mAction.mAppearance.mDescriptionGetter = description; 811 return this; 812 } 813 checked(boolean checked)814 public Builder checked(boolean checked) { 815 mAction.mAppearance.setChecked(checked); 816 return this; 817 } 818 checked(BooleanGetter getter)819 public Builder checked(BooleanGetter getter) { 820 mAction.mAppearance.setChecked(getter); 821 return this; 822 } 823 data(Bundle data)824 public Builder data(Bundle data) { 825 mAction.mActionData = data; 826 return this; 827 } 828 829 /* 830 * Makes this action default initial selection when the list is displayed. 831 */ defaultSelection()832 public Builder defaultSelection() { 833 mAction.mDefaultSelection = true; 834 return this; 835 } 836 enabled(boolean enabled)837 public Builder enabled(boolean enabled) { 838 mAction.mEnabled = enabled; 839 return this; 840 } 841 build()842 public Action build() { 843 return mAction; 844 } 845 } 846 847 @Override isEnabled()848 /* package */ boolean isEnabled() { 849 return mEnabled; 850 } 851 852 @Override getAppearance()853 /* package */ Appearance getAppearance() { 854 return mAppearance; 855 } 856 857 @Override Log(int level)858 /* package */ void Log(int level) { 859 Log.d("Layout", indent(level) + "Action #" + mActionId + " " + mAppearance); 860 } 861 getId()862 public int getId() { 863 return mActionId; 864 } 865 getIntent()866 public Intent getIntent() { 867 return mIntent; 868 } 869 870 @Override getTitle()871 public String getTitle() { 872 return mAppearance.getTitle(); 873 } 874 getData()875 public Bundle getData() { 876 return mActionData; 877 } 878 } 879 880 public static class Status extends LayoutTreeNode { 881 private final Appearance mAppearance = new Appearance(); 882 private boolean mEnabled = true; 883 884 public static class Builder { 885 private final Resources mRes; 886 private final Status mStatus = new Status(); 887 Builder(Resources res)888 public Builder(Resources res) { 889 mRes = res; 890 } 891 icon(int resId)892 public Builder icon(int resId) { 893 mStatus.mAppearance.mIcon = mRes.getDrawable(resId); 894 return this; 895 } 896 title(int resId)897 public Builder title(int resId) { 898 mStatus.mAppearance.mTitle = mRes.getString(resId); 899 return this; 900 } 901 description(int resId)902 public Builder description(int resId) { 903 mStatus.mAppearance.mDescriptionGetter = new LiteralStringGetter(mRes.getString( 904 resId)); 905 return this; 906 } 907 title(String title)908 public Builder title(String title) { 909 mStatus.mAppearance.mTitle = title; 910 return this; 911 } 912 description(String description)913 public Builder description(String description) { 914 mStatus.mAppearance.mDescriptionGetter = new LiteralStringGetter(description); 915 return this; 916 } 917 description(StringGetter description)918 public Builder description(StringGetter description) { 919 mStatus.mAppearance.mDescriptionGetter = description; 920 return this; 921 } 922 enabled(boolean enabled)923 public Builder enabled(boolean enabled) { 924 mStatus.mEnabled = enabled; 925 return this; 926 } 927 build()928 public Status build() { 929 return mStatus; 930 } 931 } 932 933 @Override getTitle()934 public String getTitle() { 935 return mAppearance.getTitle(); 936 } 937 938 @Override isEnabled()939 /* package */ boolean isEnabled() { 940 return mEnabled; 941 } 942 943 @Override getAppearance()944 /* package */ Appearance getAppearance() { 945 return mAppearance; 946 } 947 948 @Override Log(int level)949 /* package */ void Log(int level) { 950 Log.d("Layout", indent(level) + "Status " + mAppearance); 951 } 952 } 953 954 public static class Static extends LayoutTreeNode { 955 private String mTitle; 956 957 public static class Builder { 958 private final Resources mRes; 959 private final Static mStatic = new Static(); 960 Builder(Resources res)961 public Builder(Resources res) { 962 mRes = res; 963 } 964 title(int resId)965 public Builder title(int resId) { 966 mStatic.mTitle = mRes.getString(resId); 967 return this; 968 } 969 title(String title)970 public Builder title(String title) { 971 mStatic.mTitle = title; 972 return this; 973 } 974 build()975 public Static build() { 976 return mStatic; 977 } 978 } 979 980 @Override getTitle()981 public String getTitle() { 982 return mTitle; 983 } 984 985 @Override getAppearance()986 /* package */ Appearance getAppearance() { 987 return null; 988 } 989 990 @Override Log(int level)991 /* package */ void Log(int level) { 992 Log.d("Layout", indent(level) + "Static '" + mTitle + "'"); 993 } 994 } 995 996 public static class WallOfText extends LayoutTreeNode { 997 private String mTitle; 998 999 public static class Builder { 1000 private final Resources mRes; 1001 private final WallOfText mWallOfText = new WallOfText(); 1002 Builder(Resources res)1003 public Builder(Resources res) { 1004 mRes = res; 1005 } 1006 title(int resId)1007 public Builder title(int resId) { 1008 mWallOfText.mTitle = mRes.getString(resId); 1009 return this; 1010 } 1011 title(String title)1012 public Builder title(String title) { 1013 mWallOfText.mTitle = title; 1014 return this; 1015 } 1016 build()1017 public WallOfText build() { 1018 return mWallOfText; 1019 } 1020 } 1021 1022 @Override getTitle()1023 public String getTitle() { 1024 return mTitle; 1025 } 1026 1027 @Override getAppearance()1028 /* package */ Appearance getAppearance() { 1029 return null; 1030 } 1031 1032 @Override Log(int level)1033 /* package */ void Log(int level) { 1034 Log.d("Layout", indent(level) + "Static '" + mTitle + "'"); 1035 } 1036 } 1037 1038 /** 1039 * Pointer to currently visible item. 1040 */ 1041 private Header mNavigationCursor; 1042 1043 /** 1044 * Index of selected item when items are displayed. This is used by LayoutGetter to implemented 1045 * selection stability, where a LayoutGetter can arrange for a list that is refreshed regularly 1046 * to carry forward a selection. 1047 */ 1048 private int mInitialItemIndex = -1; 1049 private final ArrayList<LayoutRow> mLayoutRows = new ArrayList<>(); 1050 private final ArrayList<LayoutGetter> mVisibleLayoutGetters = new ArrayList<>(); 1051 private final ArrayList<LayoutTreeNode> mChildren = new ArrayList<>(); 1052 private String mTopLevelBreadcrumb = ""; 1053 private LayoutNodeRefreshListener mListener; 1054 getLayoutRows()1055 public ArrayList<LayoutRow> getLayoutRows() { 1056 return mLayoutRows; 1057 } 1058 setRefreshViewListener(LayoutNodeRefreshListener listener)1059 public void setRefreshViewListener(LayoutNodeRefreshListener listener) { 1060 mListener = listener; 1061 for (final LayoutGetter getter : mVisibleLayoutGetters) { 1062 getter.setListener(listener); 1063 } 1064 } 1065 1066 /** 1067 * Return the breadcrumb the user should see in the content pane. 1068 */ getBreadcrumb()1069 public String getBreadcrumb() { 1070 if (mNavigationCursor.mParent == null) { 1071 // At the top level of the layout. 1072 return mTopLevelBreadcrumb; 1073 } else { 1074 // Showing a header down the hierarchy, breadcrumb is title of item above. 1075 return ((Header) (mNavigationCursor.mParent)).mAppearance.mTitle; 1076 } 1077 } 1078 1079 /** 1080 * Navigate up one level, return true if a parent node is now visible. Return false if the 1081 * already at the top level node. The controlling fragment interprets a false return value as 1082 * "stop activity". 1083 */ goBack()1084 public boolean goBack() { 1085 if (mNavigationCursor.mParent != null) { 1086 mNavigationCursor = (Header) mNavigationCursor.mParent; 1087 updateLayoutRows(); 1088 return true; 1089 } 1090 return false; 1091 } 1092 1093 /** 1094 * Parcelable implementation. 1095 */ Layout(Parcel in)1096 public Layout(Parcel in) { 1097 } 1098 Layout()1099 public Layout() { 1100 mNavigationCursor = null; 1101 } 1102 1103 @Override describeContents()1104 public int describeContents() { 1105 return 0; 1106 } 1107 1108 @Override writeToParcel(Parcel out, int flags)1109 public void writeToParcel(Parcel out, int flags) { 1110 } 1111 1112 public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 1113 public Layout createFromParcel(Parcel in) { 1114 return new Layout(in); 1115 } 1116 1117 public Layout[] newArray(int size) { 1118 return new Layout[size]; 1119 } 1120 }; 1121 getTitle()1122 String getTitle() { 1123 return mNavigationCursor.mAppearance.mTitle; 1124 } 1125 getIcon()1126 Drawable getIcon() { 1127 return mNavigationCursor.mAppearance.getIcon(); 1128 } 1129 getDescription()1130 String getDescription() { 1131 return mNavigationCursor.getDetailedDescription(); 1132 } 1133 goToTitle(String title)1134 public void goToTitle(String title) { 1135 while (mNavigationCursor.mParent != null) { 1136 mNavigationCursor = (Header) (mNavigationCursor.mParent); 1137 if (TextUtils.equals(mNavigationCursor.mAppearance.mTitle, title)) { 1138 break; 1139 } 1140 } 1141 updateLayoutRows(); 1142 } 1143 1144 /* 1145 * Respond to a user click on "layoutRow" and return "true" if the state of the display has 1146 * changed. A controlling fragment will respond to a "true" return by updating the view. 1147 */ onClickNavigate(LayoutRow layoutRow)1148 public boolean onClickNavigate(LayoutRow layoutRow) { 1149 LayoutTreeNode node = layoutRow.mNode; 1150 if (node instanceof Header) { 1151 mNavigationCursor.mSelectedIndex = mLayoutRows.indexOf(layoutRow); 1152 mNavigationCursor = (Header) node; 1153 updateLayoutRows(); 1154 return true; 1155 } 1156 return false; 1157 } 1158 reloadLayoutRows()1159 public void reloadLayoutRows() { 1160 updateLayoutRows(); 1161 } 1162 add(Header header)1163 public Layout add(Header header) { 1164 header.mParent = null; 1165 mChildren.add(header); 1166 return this; 1167 } 1168 add(LayoutTreeNode leaf)1169 public Layout add(LayoutTreeNode leaf) { 1170 leaf.mParent = null; 1171 mChildren.add(leaf); 1172 return this; 1173 } 1174 breadcrumb(String topLevelBreadcrumb)1175 public Layout breadcrumb(String topLevelBreadcrumb) { 1176 mTopLevelBreadcrumb = topLevelBreadcrumb; 1177 return this; 1178 } 1179 1180 /** 1181 * Sets the selected node to the first top level node with its title member equal to "title". If 1182 * "title" is null, empty, or there are no top level nodes with a title member equal to "title", 1183 * set the first node in the list as the selected. 1184 */ setSelectedByTitle(String title)1185 public Layout setSelectedByTitle(String title) { 1186 for (int i = 0; i < mChildren.size(); ++i) { 1187 if (TextUtils.equals(mChildren.get(i).getTitle(), title)) { 1188 mInitialItemIndex = i; 1189 break; 1190 } 1191 } 1192 return this; 1193 } 1194 Log(int level)1195 public void Log(int level) { 1196 for (LayoutTreeNode i : mChildren) { 1197 i.Log(level + 1); 1198 } 1199 } 1200 Log()1201 public void Log() { 1202 Log.d("Layout", "----- Layout"); 1203 Log(0); 1204 } 1205 navigateToRoot()1206 public void navigateToRoot() { 1207 if (mChildren.size() > 0) { 1208 mNavigationCursor = (Header) mChildren.get(0); 1209 } else { 1210 mNavigationCursor = null; 1211 } 1212 updateLayoutRows(); 1213 } 1214 getSelectedIndex()1215 public int getSelectedIndex() { 1216 return mNavigationCursor.mSelectedIndex; 1217 } 1218 setSelectedIndex(int index)1219 public void setSelectedIndex(int index) { 1220 mNavigationCursor.mSelectedIndex = index; 1221 } 1222 setParentSelectedIndex(int index)1223 public void setParentSelectedIndex(int index) { 1224 if (mNavigationCursor.mParent != null) { 1225 Header u = (Header) mNavigationCursor.mParent; 1226 u.mSelectedIndex = index; 1227 } 1228 } 1229 addNodeListToLayoutRows(ArrayList<LayoutTreeNode> list)1230 private void addNodeListToLayoutRows(ArrayList<LayoutTreeNode> list) { 1231 for (LayoutTreeNode node : list) { 1232 if (node instanceof LayoutGetter) { 1233 // Add subitems of "node" recursively. 1234 LayoutGetter layoutGetter = (LayoutGetter) node; 1235 layoutGetter.setListener(mListener); 1236 mVisibleLayoutGetters.add(layoutGetter); 1237 Layout layout = layoutGetter.get(); 1238 for (LayoutTreeNode child : layout.mChildren) { 1239 child.mParent = mNavigationCursor; 1240 } 1241 int initialIndex = layout.mInitialItemIndex; 1242 if (initialIndex != -1) { 1243 mNavigationCursor.mSelectedIndex = mLayoutRows.size() + initialIndex; 1244 } 1245 addNodeListToLayoutRows(layout.mChildren); 1246 } else if (node instanceof SelectionGroup) { 1247 SelectionGroup sg = (SelectionGroup) node; 1248 for (int i = 0; i < sg.size(); ++i) { 1249 mLayoutRows.add(new LayoutRow(sg, i)); 1250 } 1251 } else { 1252 if (node instanceof Action && ((Action) node).mDefaultSelection) { 1253 mNavigationCursor.mSelectedIndex = mLayoutRows.size(); 1254 } 1255 mLayoutRows.add(new LayoutRow(node)); 1256 } 1257 } 1258 } 1259 updateLayoutRows()1260 private void updateLayoutRows() { 1261 mLayoutRows.clear(); 1262 for (LayoutGetter layoutGetter : mVisibleLayoutGetters) { 1263 layoutGetter.notVisible(); 1264 } 1265 mVisibleLayoutGetters.clear(); 1266 addNodeListToLayoutRows(mNavigationCursor.mChildren); 1267 1268 // Skip past any unselectable items 1269 final int rowCount = mLayoutRows.size(); 1270 while (mNavigationCursor.mSelectedIndex < rowCount - 1 && 1271 mNavigationCursor.mSelectedIndex >= 0 && 1272 ((mLayoutRows.get(mNavigationCursor.mSelectedIndex).mViewType 1273 == LayoutRow.VIEW_TYPE_STATIC) || 1274 (mLayoutRows.get(mNavigationCursor.mSelectedIndex).mViewType 1275 == LayoutRow.VIEW_TYPE_WALLOFTEXT))) { 1276 mNavigationCursor.mSelectedIndex++; 1277 } 1278 } 1279 indent(int level)1280 private static String indent(int level) { 1281 StringBuilder s = new StringBuilder(); 1282 for (int i = 0; i < level; ++i) { 1283 s.append(" "); 1284 } 1285 return s.toString(); 1286 } 1287 } 1288