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.Context; 20 import android.content.Intent; 21 import android.content.res.Resources; 22 import android.graphics.drawable.Drawable; 23 import android.net.Uri; 24 import android.os.Bundle; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.text.TextUtils; 28 import android.util.Log; 29 30 import java.util.ArrayList; 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 { 41 void onRefreshView(); 42 Node getSelectedNode(); 43 } 44 45 public interface ContentNodeRefreshListener { 46 void onRefreshView(); 47 } 48 49 public interface Node { 50 String getTitle(); 51 } 52 53 private abstract static class LayoutTreeNode implements Node { 54 LayoutTreeBranch mParent; 55 56 void Log(int level) { 57 } 58 } 59 60 private abstract static class LayoutTreeBranch extends LayoutTreeNode { 61 ArrayList<LayoutTreeNode> mChildren; 62 LayoutTreeBranch() { 63 mChildren = new ArrayList<LayoutTreeNode>(); 64 } 65 } 66 67 public static class LayoutRow { 68 public static final int NO_CHECK_SET = 0; 69 public static final int VIEW_TYPE_ACTION = 0; 70 public static final int VIEW_TYPE_STATIC = 1; 71 72 private String mTitle; 73 private StringGetter mDescription; 74 private LayoutTreeNode mNode; 75 private boolean mEnabled; 76 private int mViewType; 77 private boolean mChecked = false; 78 private Drawable mIcon = null; 79 80 public Node getNode() { 81 return mNode; 82 } 83 84 public Uri getIconUri() { 85 return null; 86 } 87 88 public Drawable getIcon() { 89 return mIcon; 90 } 91 92 public int getCheckSetId() { 93 return 0; 94 } 95 96 public boolean isChecked() { 97 return mChecked; 98 } 99 100 public void setChecked(boolean v) { 101 mChecked = v; 102 } 103 104 public boolean infoOnly() { 105 return false; 106 } 107 108 public boolean isEnabled() { 109 return mEnabled; 110 } 111 112 public boolean hasNext() { 113 return false; 114 } 115 116 public boolean hasMultilineDescription() { 117 return false; 118 } 119 120 public String getTitle() { 121 return mTitle; 122 } 123 124 public StringGetter getDescription() { 125 return mDescription; 126 } 127 128 public int getViewType() { 129 return mViewType; 130 } 131 132 public boolean isGoBack() { 133 if (mNode instanceof Action) { 134 Action a = (Action) mNode; 135 if (a.mActionId == Action.ACTION_BACK) { 136 return true; 137 } 138 } 139 return false; 140 } 141 142 public Action getUserAction() { 143 if (mNode instanceof Action) { 144 Action a = (Action) mNode; 145 if (a.mActionId != Action.ACTION_NONE) { 146 return a; 147 } 148 } 149 return null; 150 } 151 152 public int getContentIconRes() { 153 if (mNode instanceof Header) { 154 return ((Header) mNode).mContentIconRes; 155 } 156 return 0; 157 } 158 159 public LayoutRow(LayoutTreeNode node) { 160 mNode = node; 161 mViewType = VIEW_TYPE_ACTION; 162 Appearence a; 163 if (node instanceof Header) { 164 a = ((Header) node).mAppearence; 165 mEnabled = true; 166 } else if (node instanceof Action) { 167 a = ((Action) node).mAppearence; 168 mEnabled = true; 169 } else if (node instanceof Status) { 170 a = ((Status) node).mAppearence; 171 mEnabled = true; 172 } else { 173 a = null; 174 mEnabled = false; 175 if (node instanceof Static) { 176 mViewType = VIEW_TYPE_STATIC; 177 Static s = (Static) node; 178 mTitle = s.mTitle; 179 } 180 } 181 if (a != null) { 182 mTitle = a.getTitle(); 183 mDescription = a.mDescriptionGetter; 184 mIcon = a.getIcon(); 185 mChecked = a.isChecked(); 186 } 187 } 188 } 189 190 public abstract static class DrawableGetter { 191 public abstract Drawable get(); 192 193 /** 194 * Notification from client that antecedent data has changed and the drawable should be 195 * redisplayed. 196 */ 197 public void refreshView() { 198 //TODO - When implementing, ensure that multiple updates from the same event do not 199 // cause multiple view updates. 200 } 201 } 202 203 public abstract static class StringGetter { 204 private ContentNodeRefreshListener mListener; 205 206 public void setListener(ContentNodeRefreshListener listener) { 207 mListener = listener; 208 } 209 210 public abstract String get(); 211 212 /** 213 * Notification from client that antecedent data has changed and the string should be 214 * redisplayed. 215 */ 216 public void refreshView() { 217 if (mListener != null) { 218 mListener.onRefreshView(); 219 } 220 } 221 } 222 223 /** 224 * Implementation of "StringGetter" that stores and returns a literal string. 225 */ 226 private static class LiteralStringGetter extends StringGetter { 227 private final String mValue; 228 public String get() { 229 return mValue; 230 } 231 LiteralStringGetter(String value) { 232 mValue = value; 233 } 234 } 235 236 /** 237 * Implementation of "StringGetter" that stores a string resource id and returns a string. 238 */ 239 private static class ResourceStringGetter extends StringGetter { 240 private final int mStringResourceId; 241 private final Resources mRes; 242 public String get() { 243 return mRes.getString(mStringResourceId); 244 } 245 ResourceStringGetter(Resources res, int stringResourceId) { 246 mRes = res; 247 mStringResourceId = stringResourceId; 248 } 249 } 250 251 public abstract static class LayoutGetter extends LayoutTreeNode { 252 private LayoutNodeRefreshListener mListener; 253 254 public void setListener(LayoutNodeRefreshListener listener) { 255 mListener = listener; 256 } 257 258 public void notVisible() { 259 mListener = null; 260 onMovedOffScreen(); 261 } 262 263 public abstract Layout get(); 264 265 public Node getSelectedNode() { 266 if (mListener != null) { 267 return mListener.getSelectedNode(); 268 } else { 269 return null; 270 } 271 } 272 273 /** 274 * Notification to client that the list containting the contents of this getter is no longer 275 * visible and background tasks to update the getter contents should be stopped until the 276 * next "get()" call. 277 */ 278 public void onMovedOffScreen() { 279 } 280 281 /** 282 * Notification from client that antecedent data has changed and the list containing the 283 * contents of this getter should be updated. 284 */ 285 public void refreshView() { 286 if (mListener != null) { 287 mListener.onRefreshView(); 288 } 289 } 290 291 @Override 292 public String getTitle() { 293 return null; 294 } 295 296 void Log(int level) { 297 Log.d("Layout", indent(level) + "LayoutGetter"); 298 Layout l = get(); 299 l.Log(level + 1); 300 } 301 } 302 303 private static class Appearence { 304 private Drawable mIcon; 305 private DrawableGetter mIconGetter; 306 private String mTitle; 307 private StringGetter mDescriptionGetter; 308 private boolean mChecked = false; 309 310 public String toString() { 311 StringBuilder stringBuilder = new StringBuilder() 312 .append("'") 313 .append(mTitle) 314 .append("'"); 315 if (mDescriptionGetter != null) { 316 stringBuilder 317 .append(" : '") 318 .append(mDescriptionGetter.get()) 319 .append("'"); 320 } 321 stringBuilder 322 .append(" : '") 323 .append(mChecked) 324 .append("'"); 325 return stringBuilder.toString(); 326 } 327 328 public String getTitle() { 329 return mTitle; 330 } 331 332 public Drawable getIcon() { 333 if (mIconGetter != null) { 334 return mIconGetter.get(); 335 } else { 336 return mIcon; 337 } 338 } 339 340 public boolean isChecked() { 341 return mChecked; 342 } 343 } 344 345 /** 346 * Header is a container for a sub-menu of "LayoutTreeNode" items. 347 */ 348 public static class Header extends LayoutTreeBranch { 349 private Appearence mAppearence = new Appearence(); 350 private int mSelectedIndex = 0; 351 private String mDetailedDescription; 352 private int mContentIconRes = 0; 353 354 public static class Builder { 355 private Resources mRes; 356 private Header mHeader = new Header(); 357 358 public Builder(Resources res) { 359 mRes = res; 360 } 361 362 public Builder icon(int resId) { 363 mHeader.mAppearence.mIcon = mRes.getDrawable(resId); 364 return this; 365 } 366 367 public Builder icon(DrawableGetter drawableGetter) { 368 mHeader.mAppearence.mIconGetter = drawableGetter; 369 return this; 370 } 371 372 public Builder contentIconRes(int resId) { 373 mHeader.mContentIconRes = resId; 374 return this; 375 } 376 377 public Builder title(int resId) { 378 mHeader.mAppearence.mTitle = mRes.getString(resId); 379 return this; 380 } 381 382 public Builder description(int resId) { 383 mHeader.mAppearence.mDescriptionGetter = new ResourceStringGetter(mRes, resId); 384 return this; 385 } 386 387 public Builder title(String title) { 388 mHeader.mAppearence.mTitle = title; 389 return this; 390 } 391 392 public Builder description(String description) { 393 mHeader.mAppearence.mDescriptionGetter = new LiteralStringGetter(description); 394 return this; 395 } 396 397 public Builder description(StringGetter description) { 398 mHeader.mAppearence.mDescriptionGetter = description; 399 return this; 400 } 401 402 public Builder detailedDescription(int resId) { 403 mHeader.mDetailedDescription = mRes.getString(resId); 404 return this; 405 } 406 407 public Builder detailedDescription(String detailedDescription) { 408 mHeader.mDetailedDescription = detailedDescription; 409 return this; 410 } 411 412 public Header build() { 413 return mHeader; 414 } 415 } 416 417 @Override 418 public String getTitle() { 419 return mAppearence.getTitle(); 420 } 421 422 public Header add(LayoutTreeNode node) { 423 node.mParent = this; 424 mChildren.add(node); 425 return this; 426 } 427 428 String getDetailedDescription() { 429 return mDetailedDescription; 430 } 431 432 void Log(int level) { 433 Log.d("Layout", indent(level) + "Header " + mAppearence); 434 for (LayoutTreeNode i : mChildren) 435 i.Log(level + 1); 436 } 437 } 438 439 public static class Action extends LayoutTreeNode { 440 public static final int ACTION_NONE = -1; 441 public static final int ACTION_INTENT = -2; 442 public static final int ACTION_BACK = -3; 443 private int mActionId; 444 private Intent mIntent; 445 private Appearence mAppearence = new Appearence(); 446 private Bundle mActionData; 447 private boolean mDefaultSelection = false; 448 449 private Action(int id) { 450 mActionId = id; 451 } 452 453 private Action(Intent intent) { 454 mActionId = ACTION_INTENT; 455 mIntent = intent; 456 } 457 458 public static class Builder { 459 private Resources mRes; 460 private Action mAction; 461 462 public Builder(Resources res, int id) { 463 mRes = res; 464 mAction = new Action(id); 465 } 466 467 public Builder(Resources res, Intent intent) { 468 mRes = res; 469 mAction = new Action(intent); 470 } 471 472 public Builder title(int resId) { 473 mAction.mAppearence.mTitle = mRes.getString(resId); 474 return this; 475 } 476 477 public Builder description(int resId) { 478 mAction.mAppearence.mDescriptionGetter = new LiteralStringGetter(mRes.getString( 479 resId)); 480 return this; 481 } 482 483 public Builder title(String title) { 484 mAction.mAppearence.mTitle = title; 485 return this; 486 } 487 488 public Builder description(String description) { 489 mAction.mAppearence.mDescriptionGetter = new LiteralStringGetter(description); 490 return this; 491 } 492 493 public Builder checked(boolean checked) { 494 mAction.mAppearence.mChecked = checked; 495 return this; 496 } 497 498 public Builder data(Bundle data) { 499 mAction.mActionData = data; 500 return this; 501 } 502 503 /* 504 * Makes this action default initial selection when the list is displayed. 505 */ 506 public Builder defaultSelection() { 507 mAction.mDefaultSelection = true; 508 return this; 509 } 510 511 public Action build() { 512 return mAction; 513 } 514 } 515 516 void Log(int level) { 517 Log.d("Layout", indent(level) + "Action #" + mActionId + " " + mAppearence); 518 } 519 520 public int getId() { 521 return mActionId; 522 } 523 524 public Intent getIntent() { 525 return mIntent; 526 } 527 528 @Override 529 public String getTitle() { 530 return mAppearence.getTitle(); 531 } 532 533 public Bundle getData() { 534 return mActionData; 535 } 536 } 537 538 public static class Status extends LayoutTreeNode { 539 private Appearence mAppearence = new Appearence(); 540 541 public static class Builder { 542 private Resources mRes; 543 private Status mStatus = new Status(); 544 545 public Builder(Resources res) { 546 mRes = res; 547 } 548 549 public Builder icon(int resId) { 550 mStatus.mAppearence.mIcon = mRes.getDrawable(resId); 551 return this; 552 } 553 554 public Builder title(int resId) { 555 mStatus.mAppearence.mTitle = mRes.getString(resId); 556 return this; 557 } 558 559 public Builder description(int resId) { 560 mStatus.mAppearence.mDescriptionGetter = new LiteralStringGetter(mRes.getString( 561 resId)); 562 return this; 563 } 564 565 public Builder title(String title) { 566 mStatus.mAppearence.mTitle = title; 567 return this; 568 } 569 570 public Builder description(String description) { 571 mStatus.mAppearence.mDescriptionGetter = new LiteralStringGetter(description); 572 return this; 573 } 574 575 public Builder description(StringGetter description) { 576 mStatus.mAppearence.mDescriptionGetter = description; 577 return this; 578 } 579 580 public Status build() { 581 return mStatus; 582 } 583 } 584 585 @Override 586 public String getTitle() { 587 return mAppearence.getTitle(); 588 } 589 590 void Log(int level) { 591 Log.d("Layout", indent(level) + "Status " + mAppearence); 592 } 593 } 594 595 public static class Static extends LayoutTreeNode { 596 private String mTitle; 597 598 public static class Builder { 599 private Resources mRes; 600 private Static mStatic = new Static(); 601 602 public Builder(Resources res) { 603 mRes = res; 604 } 605 606 public Builder title(int resId) { 607 mStatic.mTitle = mRes.getString(resId); 608 return this; 609 } 610 611 public Builder title(String title) { 612 mStatic.mTitle = title; 613 return this; 614 } 615 616 public Static build() { 617 return mStatic; 618 } 619 } 620 621 @Override 622 public String getTitle() { 623 return mTitle; 624 } 625 626 void Log(int level) { 627 Log.d("Layout", indent(level) + "Static '" + mTitle + "'"); 628 } 629 } 630 631 /** 632 * Pointer to currently visible item. 633 */ 634 private Header mNavigationCursor; 635 636 /** 637 * Index of selected item when items are displayed. This is used by LayoutGetter to implemented 638 * selection stability, where a LayoutGetter can arrange for a list that is refreshed regularly 639 * to carry forward a selection. 640 */ 641 private int mInitialItemIndex = -1; 642 private final ArrayList<LayoutRow> mLayoutRows = new ArrayList<LayoutRow>(); 643 private final ArrayList<LayoutGetter> mVisibleLayoutGetters = new ArrayList<LayoutGetter>(); 644 private final ArrayList<LayoutTreeNode> mChildren = new ArrayList<LayoutTreeNode>(); 645 private String mTopLevelBreadcrumb = ""; 646 private LayoutNodeRefreshListener mListener; 647 648 public ArrayList<LayoutRow> getLayoutRows() { 649 return mLayoutRows; 650 } 651 652 public void setRefreshViewListener(LayoutNodeRefreshListener listener) { 653 mListener = listener; 654 } 655 656 /** 657 * Return the breadcrumb the user should see in the content pane. 658 */ 659 public String getBreadcrumb() { 660 if (mNavigationCursor.mParent == null) { 661 // At the top level of the layout. 662 return mTopLevelBreadcrumb; 663 } else { 664 // Showing a header down the hierarchy, breadcrumb is title of item above. 665 return ((Header) (mNavigationCursor.mParent)).mAppearence.mTitle; 666 } 667 } 668 669 /** 670 * Navigate up one level, return true if a parent node is now visible. Return false if the 671 * already at the top level node. The controlling fragment interprets a false return value as 672 * "stop activity". 673 */ 674 public boolean goBack() { 675 if (mNavigationCursor.mParent != null) { 676 Header u = (Header) mNavigationCursor.mParent; 677 if (u != null) { 678 mNavigationCursor = u; 679 updateLayoutRows(); 680 return true; 681 } 682 } 683 return false; 684 } 685 686 /** 687 * Parcelable implementation. 688 */ 689 public Layout(Parcel in) { 690 } 691 692 public Layout() { 693 mNavigationCursor = null; 694 } 695 696 @Override 697 public int describeContents() { 698 return 0; 699 } 700 701 @Override 702 public void writeToParcel(Parcel out, int flags) { 703 } 704 705 public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 706 public Layout createFromParcel(Parcel in) { 707 return new Layout(in); 708 } 709 710 public Layout[] newArray(int size) { 711 return new Layout[size]; 712 } 713 }; 714 715 String getTitle() { 716 return mNavigationCursor.mAppearence.mTitle; 717 } 718 719 Drawable getIcon() { 720 return mNavigationCursor.mAppearence.getIcon(); 721 } 722 723 String getDescription() { 724 return mNavigationCursor.getDetailedDescription(); 725 } 726 727 public void goToTitle(String title) { 728 while (mNavigationCursor.mParent != null) { 729 mNavigationCursor = (Header) (mNavigationCursor.mParent); 730 if (TextUtils.equals(mNavigationCursor.mAppearence.mTitle, title)) { 731 break; 732 } 733 } 734 updateLayoutRows(); 735 } 736 737 /* 738 * Respond to a user click on "layoutRow" and return "true" if the state of the display has 739 * changed. A controlling fragment will respond to a "true" return by updating the view. 740 */ 741 public boolean onClickNavigate(LayoutRow layoutRow) { 742 LayoutTreeNode node = layoutRow.mNode; 743 if (node instanceof Header) { 744 mNavigationCursor.mSelectedIndex = mLayoutRows.indexOf(layoutRow); 745 mNavigationCursor = (Header) node; 746 updateLayoutRows(); 747 return true; 748 } 749 return false; 750 } 751 752 public void reloadLayoutRows() { 753 updateLayoutRows(); 754 } 755 756 public Layout add(Header header) { 757 header.mParent = null; 758 mChildren.add(header); 759 return this; 760 } 761 762 public Layout add(LayoutTreeNode leaf) { 763 leaf.mParent = null; 764 mChildren.add(leaf); 765 return this; 766 } 767 768 public Layout breadcrumb(String topLevelBreadcrumb) { 769 mTopLevelBreadcrumb = topLevelBreadcrumb; 770 return this; 771 } 772 773 /** 774 * Sets the selected node to the first top level node with its title member equal to "title". If 775 * "title" is null, empty, or there are no top level nodes with a title member equal to "title", 776 * set the first node in the list as the selected. 777 */ 778 public Layout setSelectedByTitle(String title) { 779 for (int i = 0; i < mChildren.size(); ++i) { 780 if (TextUtils.equals(mChildren.get(i).getTitle(), title)) { 781 mInitialItemIndex = i; 782 break; 783 } 784 } 785 return this; 786 } 787 788 public void Log(int level) { 789 for (LayoutTreeNode i : mChildren) { 790 i.Log(level + 1); 791 } 792 } 793 794 public void Log() { 795 Log.d("Layout", "----- Layout"); 796 Log(0); 797 } 798 799 public void navigateToRoot() { 800 if (mChildren.size() > 0) { 801 mNavigationCursor = (Header) mChildren.get(0); 802 } else { 803 mNavigationCursor = null; 804 } 805 updateLayoutRows(); 806 } 807 808 public int getSelectedIndex() { 809 return mNavigationCursor.mSelectedIndex; 810 } 811 812 public void setSelectedIndex(int index) { 813 mNavigationCursor.mSelectedIndex = index; 814 } 815 816 public void setParentSelectedIndex(int index) { 817 if (mNavigationCursor.mParent != null) { 818 Header u = (Header) mNavigationCursor.mParent; 819 u.mSelectedIndex = index; 820 } 821 } 822 823 private void addNodeListToLayoutRows(ArrayList<LayoutTreeNode> list) { 824 for (LayoutTreeNode node : list) { 825 if (node instanceof LayoutGetter) { 826 // Add subitems of "node" recursively. 827 LayoutGetter layoutGetter = (LayoutGetter) node; 828 layoutGetter.setListener(mListener); 829 mVisibleLayoutGetters.add(layoutGetter); 830 Layout layout = layoutGetter.get(); 831 for (LayoutTreeNode child : layout.mChildren) { 832 child.mParent = mNavigationCursor; 833 } 834 int initialIndex = layout.mInitialItemIndex; 835 if (initialIndex != -1) { 836 mNavigationCursor.mSelectedIndex = mLayoutRows.size() + initialIndex; 837 } 838 addNodeListToLayoutRows(layout.mChildren); 839 } else { 840 if (node instanceof Action && ((Action) node).mDefaultSelection) { 841 mNavigationCursor.mSelectedIndex = mLayoutRows.size(); 842 } 843 mLayoutRows.add(new LayoutRow(node)); 844 } 845 } 846 } 847 848 private void updateLayoutRows() { 849 mLayoutRows.clear(); 850 for (LayoutGetter layoutGetter : mVisibleLayoutGetters) { 851 layoutGetter.notVisible(); 852 } 853 mVisibleLayoutGetters.clear(); 854 addNodeListToLayoutRows(mNavigationCursor.mChildren); 855 } 856 857 private static String indent(int level) { 858 String s = new String(); 859 for (int i = 0; i < level; ++i) { 860 s += " "; 861 } 862 return s; 863 } 864 } 865