mobile/android/base/home/HomeConfig.java

branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
equal deleted inserted replaced
-1:000000000000 0:4b875ad663f4
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 package org.mozilla.gecko.home;
7
8 import org.mozilla.gecko.GeckoAppShell;
9 import org.mozilla.gecko.GeckoEvent;
10 import org.mozilla.gecko.R;
11 import org.mozilla.gecko.util.ThreadUtils;
12
13 import org.json.JSONArray;
14 import org.json.JSONException;
15 import org.json.JSONObject;
16
17 import android.content.Context;
18 import android.os.Parcel;
19 import android.os.Parcelable;
20 import android.text.TextUtils;
21
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.EnumSet;
25 import java.util.HashMap;
26 import java.util.Iterator;
27 import java.util.LinkedList;
28 import java.util.List;
29 import java.util.Map;
30
31 public final class HomeConfig {
32 /**
33 * Used to determine what type of HomeFragment subclass to use when creating
34 * a given panel. With the exception of DYNAMIC, all of these types correspond
35 * to a default set of built-in panels. The DYNAMIC panel type is used by
36 * third-party services to create panels with varying types of content.
37 */
38 public static enum PanelType implements Parcelable {
39 TOP_SITES("top_sites", TopSitesPanel.class),
40 BOOKMARKS("bookmarks", BookmarksPanel.class),
41 HISTORY("history", HistoryPanel.class),
42 READING_LIST("reading_list", ReadingListPanel.class),
43 DYNAMIC("dynamic", DynamicPanel.class);
44
45 private final String mId;
46 private final Class<?> mPanelClass;
47
48 PanelType(String id, Class<?> panelClass) {
49 mId = id;
50 mPanelClass = panelClass;
51 }
52
53 public static PanelType fromId(String id) {
54 if (id == null) {
55 throw new IllegalArgumentException("Could not convert null String to PanelType");
56 }
57
58 for (PanelType panelType : PanelType.values()) {
59 if (TextUtils.equals(panelType.mId, id.toLowerCase())) {
60 return panelType;
61 }
62 }
63
64 throw new IllegalArgumentException("Could not convert String id to PanelType");
65 }
66
67 @Override
68 public String toString() {
69 return mId;
70 }
71
72 public Class<?> getPanelClass() {
73 return mPanelClass;
74 }
75
76 @Override
77 public int describeContents() {
78 return 0;
79 }
80
81 @Override
82 public void writeToParcel(Parcel dest, int flags) {
83 dest.writeInt(ordinal());
84 }
85
86 public static final Creator<PanelType> CREATOR = new Creator<PanelType>() {
87 @Override
88 public PanelType createFromParcel(final Parcel source) {
89 return PanelType.values()[source.readInt()];
90 }
91
92 @Override
93 public PanelType[] newArray(final int size) {
94 return new PanelType[size];
95 }
96 };
97 }
98
99 public static class PanelConfig implements Parcelable {
100 private final PanelType mType;
101 private final String mTitle;
102 private final String mId;
103 private final LayoutType mLayoutType;
104 private final List<ViewConfig> mViews;
105 private final AuthConfig mAuthConfig;
106 private final EnumSet<Flags> mFlags;
107
108 private static final String JSON_KEY_TYPE = "type";
109 private static final String JSON_KEY_TITLE = "title";
110 private static final String JSON_KEY_ID = "id";
111 private static final String JSON_KEY_LAYOUT = "layout";
112 private static final String JSON_KEY_VIEWS = "views";
113 private static final String JSON_KEY_AUTH_CONFIG = "authConfig";
114 private static final String JSON_KEY_DEFAULT = "default";
115 private static final String JSON_KEY_DISABLED = "disabled";
116
117 public enum Flags {
118 DEFAULT_PANEL,
119 DISABLED_PANEL
120 }
121
122 public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException {
123 final String panelType = json.optString(JSON_KEY_TYPE, null);
124 if (TextUtils.isEmpty(panelType)) {
125 mType = PanelType.DYNAMIC;
126 } else {
127 mType = PanelType.fromId(panelType);
128 }
129
130 mTitle = json.getString(JSON_KEY_TITLE);
131 mId = json.getString(JSON_KEY_ID);
132
133 final String layoutTypeId = json.optString(JSON_KEY_LAYOUT, null);
134 if (layoutTypeId != null) {
135 mLayoutType = LayoutType.fromId(layoutTypeId);
136 } else {
137 mLayoutType = null;
138 }
139
140 final JSONArray jsonViews = json.optJSONArray(JSON_KEY_VIEWS);
141 if (jsonViews != null) {
142 mViews = new ArrayList<ViewConfig>();
143
144 final int viewCount = jsonViews.length();
145 for (int i = 0; i < viewCount; i++) {
146 final JSONObject jsonViewConfig = (JSONObject) jsonViews.get(i);
147 final ViewConfig viewConfig = new ViewConfig(i, jsonViewConfig);
148 mViews.add(viewConfig);
149 }
150 } else {
151 mViews = null;
152 }
153
154 final JSONObject jsonAuthConfig = json.optJSONObject(JSON_KEY_AUTH_CONFIG);
155 if (jsonAuthConfig != null) {
156 mAuthConfig = new AuthConfig(jsonAuthConfig);
157 } else {
158 mAuthConfig = null;
159 }
160
161 mFlags = EnumSet.noneOf(Flags.class);
162
163 if (json.optBoolean(JSON_KEY_DEFAULT, false)) {
164 mFlags.add(Flags.DEFAULT_PANEL);
165 }
166
167 if (json.optBoolean(JSON_KEY_DISABLED, false)) {
168 mFlags.add(Flags.DISABLED_PANEL);
169 }
170
171 validate();
172 }
173
174 @SuppressWarnings("unchecked")
175 public PanelConfig(Parcel in) {
176 mType = (PanelType) in.readParcelable(getClass().getClassLoader());
177 mTitle = in.readString();
178 mId = in.readString();
179 mLayoutType = (LayoutType) in.readParcelable(getClass().getClassLoader());
180
181 mViews = new ArrayList<ViewConfig>();
182 in.readTypedList(mViews, ViewConfig.CREATOR);
183
184 mAuthConfig = (AuthConfig) in.readParcelable(getClass().getClassLoader());
185
186 mFlags = (EnumSet<Flags>) in.readSerializable();
187
188 validate();
189 }
190
191 public PanelConfig(PanelConfig panelConfig) {
192 mType = panelConfig.mType;
193 mTitle = panelConfig.mTitle;
194 mId = panelConfig.mId;
195 mLayoutType = panelConfig.mLayoutType;
196
197 mViews = new ArrayList<ViewConfig>();
198 List<ViewConfig> viewConfigs = panelConfig.mViews;
199 if (viewConfigs != null) {
200 for (ViewConfig viewConfig : viewConfigs) {
201 mViews.add(new ViewConfig(viewConfig));
202 }
203 }
204
205 mAuthConfig = panelConfig.mAuthConfig;
206 mFlags = panelConfig.mFlags.clone();
207
208 validate();
209 }
210
211 public PanelConfig(PanelType type, String title, String id) {
212 this(type, title, id, EnumSet.noneOf(Flags.class));
213 }
214
215 public PanelConfig(PanelType type, String title, String id, EnumSet<Flags> flags) {
216 this(type, title, id, null, null, null, flags);
217 }
218
219 public PanelConfig(PanelType type, String title, String id, LayoutType layoutType,
220 List<ViewConfig> views, AuthConfig authConfig, EnumSet<Flags> flags) {
221 mType = type;
222 mTitle = title;
223 mId = id;
224 mLayoutType = layoutType;
225 mViews = views;
226 mAuthConfig = authConfig;
227 mFlags = flags;
228
229 validate();
230 }
231
232 private void validate() {
233 if (mType == null) {
234 throw new IllegalArgumentException("Can't create PanelConfig with null type");
235 }
236
237 if (TextUtils.isEmpty(mTitle)) {
238 throw new IllegalArgumentException("Can't create PanelConfig with empty title");
239 }
240
241 if (TextUtils.isEmpty(mId)) {
242 throw new IllegalArgumentException("Can't create PanelConfig with empty id");
243 }
244
245 if (mLayoutType == null && mType == PanelType.DYNAMIC) {
246 throw new IllegalArgumentException("Can't create a dynamic PanelConfig with null layout type");
247 }
248
249 if ((mViews == null || mViews.size() == 0) && mType == PanelType.DYNAMIC) {
250 throw new IllegalArgumentException("Can't create a dynamic PanelConfig with no views");
251 }
252
253 if (mFlags == null) {
254 throw new IllegalArgumentException("Can't create PanelConfig with null flags");
255 }
256 }
257
258 public PanelType getType() {
259 return mType;
260 }
261
262 public String getTitle() {
263 return mTitle;
264 }
265
266 public String getId() {
267 return mId;
268 }
269
270 public LayoutType getLayoutType() {
271 return mLayoutType;
272 }
273
274 public int getViewCount() {
275 return (mViews != null ? mViews.size() : 0);
276 }
277
278 public ViewConfig getViewAt(int index) {
279 return (mViews != null ? mViews.get(index) : null);
280 }
281
282 public boolean isDynamic() {
283 return (mType == PanelType.DYNAMIC);
284 }
285
286 public boolean isDefault() {
287 return mFlags.contains(Flags.DEFAULT_PANEL);
288 }
289
290 private void setIsDefault(boolean isDefault) {
291 if (isDefault) {
292 mFlags.add(Flags.DEFAULT_PANEL);
293 } else {
294 mFlags.remove(Flags.DEFAULT_PANEL);
295 }
296 }
297
298 public boolean isDisabled() {
299 return mFlags.contains(Flags.DISABLED_PANEL);
300 }
301
302 private void setIsDisabled(boolean isDisabled) {
303 if (isDisabled) {
304 mFlags.add(Flags.DISABLED_PANEL);
305 } else {
306 mFlags.remove(Flags.DISABLED_PANEL);
307 }
308 }
309
310 public AuthConfig getAuthConfig() {
311 return mAuthConfig;
312 }
313
314 public JSONObject toJSON() throws JSONException {
315 final JSONObject json = new JSONObject();
316
317 json.put(JSON_KEY_TYPE, mType.toString());
318 json.put(JSON_KEY_TITLE, mTitle);
319 json.put(JSON_KEY_ID, mId);
320
321 if (mLayoutType != null) {
322 json.put(JSON_KEY_LAYOUT, mLayoutType.toString());
323 }
324
325 if (mViews != null) {
326 final JSONArray jsonViews = new JSONArray();
327
328 final int viewCount = mViews.size();
329 for (int i = 0; i < viewCount; i++) {
330 final ViewConfig viewConfig = mViews.get(i);
331 final JSONObject jsonViewConfig = viewConfig.toJSON();
332 jsonViews.put(jsonViewConfig);
333 }
334
335 json.put(JSON_KEY_VIEWS, jsonViews);
336 }
337
338 if (mAuthConfig != null) {
339 json.put(JSON_KEY_AUTH_CONFIG, mAuthConfig.toJSON());
340 }
341
342 if (mFlags.contains(Flags.DEFAULT_PANEL)) {
343 json.put(JSON_KEY_DEFAULT, true);
344 }
345
346 if (mFlags.contains(Flags.DISABLED_PANEL)) {
347 json.put(JSON_KEY_DISABLED, true);
348 }
349
350 return json;
351 }
352
353 @Override
354 public boolean equals(Object o) {
355 if (o == null) {
356 return false;
357 }
358
359 if (this == o) {
360 return true;
361 }
362
363 if (!(o instanceof PanelConfig)) {
364 return false;
365 }
366
367 final PanelConfig other = (PanelConfig) o;
368 return mId.equals(other.mId);
369 }
370
371 @Override
372 public int describeContents() {
373 return 0;
374 }
375
376 @Override
377 public void writeToParcel(Parcel dest, int flags) {
378 dest.writeParcelable(mType, 0);
379 dest.writeString(mTitle);
380 dest.writeString(mId);
381 dest.writeParcelable(mLayoutType, 0);
382 dest.writeTypedList(mViews);
383 dest.writeParcelable(mAuthConfig, 0);
384 dest.writeSerializable(mFlags);
385 }
386
387 public static final Creator<PanelConfig> CREATOR = new Creator<PanelConfig>() {
388 @Override
389 public PanelConfig createFromParcel(final Parcel in) {
390 return new PanelConfig(in);
391 }
392
393 @Override
394 public PanelConfig[] newArray(final int size) {
395 return new PanelConfig[size];
396 }
397 };
398 }
399
400 public static enum LayoutType implements Parcelable {
401 FRAME("frame");
402
403 private final String mId;
404
405 LayoutType(String id) {
406 mId = id;
407 }
408
409 public static LayoutType fromId(String id) {
410 if (id == null) {
411 throw new IllegalArgumentException("Could not convert null String to LayoutType");
412 }
413
414 for (LayoutType layoutType : LayoutType.values()) {
415 if (TextUtils.equals(layoutType.mId, id.toLowerCase())) {
416 return layoutType;
417 }
418 }
419
420 throw new IllegalArgumentException("Could not convert String id to LayoutType");
421 }
422
423 @Override
424 public String toString() {
425 return mId;
426 }
427
428 @Override
429 public int describeContents() {
430 return 0;
431 }
432
433 @Override
434 public void writeToParcel(Parcel dest, int flags) {
435 dest.writeInt(ordinal());
436 }
437
438 public static final Creator<LayoutType> CREATOR = new Creator<LayoutType>() {
439 @Override
440 public LayoutType createFromParcel(final Parcel source) {
441 return LayoutType.values()[source.readInt()];
442 }
443
444 @Override
445 public LayoutType[] newArray(final int size) {
446 return new LayoutType[size];
447 }
448 };
449 }
450
451 public static enum ViewType implements Parcelable {
452 LIST("list"),
453 GRID("grid");
454
455 private final String mId;
456
457 ViewType(String id) {
458 mId = id;
459 }
460
461 public static ViewType fromId(String id) {
462 if (id == null) {
463 throw new IllegalArgumentException("Could not convert null String to ViewType");
464 }
465
466 for (ViewType viewType : ViewType.values()) {
467 if (TextUtils.equals(viewType.mId, id.toLowerCase())) {
468 return viewType;
469 }
470 }
471
472 throw new IllegalArgumentException("Could not convert String id to ViewType");
473 }
474
475 @Override
476 public String toString() {
477 return mId;
478 }
479
480 @Override
481 public int describeContents() {
482 return 0;
483 }
484
485 @Override
486 public void writeToParcel(Parcel dest, int flags) {
487 dest.writeInt(ordinal());
488 }
489
490 public static final Creator<ViewType> CREATOR = new Creator<ViewType>() {
491 @Override
492 public ViewType createFromParcel(final Parcel source) {
493 return ViewType.values()[source.readInt()];
494 }
495
496 @Override
497 public ViewType[] newArray(final int size) {
498 return new ViewType[size];
499 }
500 };
501 }
502
503 public static enum ItemType implements Parcelable {
504 ARTICLE("article"),
505 IMAGE("image");
506
507 private final String mId;
508
509 ItemType(String id) {
510 mId = id;
511 }
512
513 public static ItemType fromId(String id) {
514 if (id == null) {
515 throw new IllegalArgumentException("Could not convert null String to ItemType");
516 }
517
518 for (ItemType itemType : ItemType.values()) {
519 if (TextUtils.equals(itemType.mId, id.toLowerCase())) {
520 return itemType;
521 }
522 }
523
524 throw new IllegalArgumentException("Could not convert String id to ItemType");
525 }
526
527 @Override
528 public String toString() {
529 return mId;
530 }
531
532 @Override
533 public int describeContents() {
534 return 0;
535 }
536
537 @Override
538 public void writeToParcel(Parcel dest, int flags) {
539 dest.writeInt(ordinal());
540 }
541
542 public static final Creator<ItemType> CREATOR = new Creator<ItemType>() {
543 @Override
544 public ItemType createFromParcel(final Parcel source) {
545 return ItemType.values()[source.readInt()];
546 }
547
548 @Override
549 public ItemType[] newArray(final int size) {
550 return new ItemType[size];
551 }
552 };
553 }
554
555 public static enum ItemHandler implements Parcelable {
556 BROWSER("browser"),
557 INTENT("intent");
558
559 private final String mId;
560
561 ItemHandler(String id) {
562 mId = id;
563 }
564
565 public static ItemHandler fromId(String id) {
566 if (id == null) {
567 throw new IllegalArgumentException("Could not convert null String to ItemHandler");
568 }
569
570 for (ItemHandler itemHandler : ItemHandler.values()) {
571 if (TextUtils.equals(itemHandler.mId, id.toLowerCase())) {
572 return itemHandler;
573 }
574 }
575
576 throw new IllegalArgumentException("Could not convert String id to ItemHandler");
577 }
578
579 @Override
580 public String toString() {
581 return mId;
582 }
583
584 @Override
585 public int describeContents() {
586 return 0;
587 }
588
589 @Override
590 public void writeToParcel(Parcel dest, int flags) {
591 dest.writeInt(ordinal());
592 }
593
594 public static final Creator<ItemHandler> CREATOR = new Creator<ItemHandler>() {
595 @Override
596 public ItemHandler createFromParcel(final Parcel source) {
597 return ItemHandler.values()[source.readInt()];
598 }
599
600 @Override
601 public ItemHandler[] newArray(final int size) {
602 return new ItemHandler[size];
603 }
604 };
605 }
606
607 public static class ViewConfig implements Parcelable {
608 private final int mIndex;
609 private final ViewType mType;
610 private final String mDatasetId;
611 private final ItemType mItemType;
612 private final ItemHandler mItemHandler;
613 private final String mBackImageUrl;
614 private final String mFilter;
615 private final EmptyViewConfig mEmptyViewConfig;
616 private final EnumSet<Flags> mFlags;
617
618 private static final String JSON_KEY_TYPE = "type";
619 private static final String JSON_KEY_DATASET = "dataset";
620 private static final String JSON_KEY_ITEM_TYPE = "itemType";
621 private static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
622 private static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
623 private static final String JSON_KEY_FILTER = "filter";
624 private static final String JSON_KEY_EMPTY = "empty";
625 private static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled";
626
627 public enum Flags {
628 REFRESH_ENABLED
629 }
630
631 public ViewConfig(int index, JSONObject json) throws JSONException, IllegalArgumentException {
632 mIndex = index;
633 mType = ViewType.fromId(json.getString(JSON_KEY_TYPE));
634 mDatasetId = json.getString(JSON_KEY_DATASET);
635 mItemType = ItemType.fromId(json.getString(JSON_KEY_ITEM_TYPE));
636 mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER));
637 mBackImageUrl = json.optString(JSON_KEY_BACK_IMAGE_URL, null);
638 mFilter = json.optString(JSON_KEY_FILTER, null);
639
640 final JSONObject jsonEmptyViewConfig = json.optJSONObject(JSON_KEY_EMPTY);
641 if (jsonEmptyViewConfig != null) {
642 mEmptyViewConfig = new EmptyViewConfig(jsonEmptyViewConfig);
643 } else {
644 mEmptyViewConfig = null;
645 }
646
647 mFlags = EnumSet.noneOf(Flags.class);
648 if (json.optBoolean(JSON_KEY_REFRESH_ENABLED, false)) {
649 mFlags.add(Flags.REFRESH_ENABLED);
650 }
651
652 validate();
653 }
654
655 @SuppressWarnings("unchecked")
656 public ViewConfig(Parcel in) {
657 mIndex = in.readInt();
658 mType = (ViewType) in.readParcelable(getClass().getClassLoader());
659 mDatasetId = in.readString();
660 mItemType = (ItemType) in.readParcelable(getClass().getClassLoader());
661 mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader());
662 mBackImageUrl = in.readString();
663 mFilter = in.readString();
664 mEmptyViewConfig = (EmptyViewConfig) in.readParcelable(getClass().getClassLoader());
665 mFlags = (EnumSet<Flags>) in.readSerializable();
666
667 validate();
668 }
669
670 public ViewConfig(ViewConfig viewConfig) {
671 mIndex = viewConfig.mIndex;
672 mType = viewConfig.mType;
673 mDatasetId = viewConfig.mDatasetId;
674 mItemType = viewConfig.mItemType;
675 mItemHandler = viewConfig.mItemHandler;
676 mBackImageUrl = viewConfig.mBackImageUrl;
677 mFilter = viewConfig.mFilter;
678 mEmptyViewConfig = viewConfig.mEmptyViewConfig;
679 mFlags = viewConfig.mFlags.clone();
680
681 validate();
682 }
683
684 public ViewConfig(int index, ViewType type, String datasetId, ItemType itemType,
685 ItemHandler itemHandler, String backImageUrl, String filter,
686 EmptyViewConfig emptyViewConfig, EnumSet<Flags> flags) {
687 mIndex = index;
688 mType = type;
689 mDatasetId = datasetId;
690 mItemType = itemType;
691 mItemHandler = itemHandler;
692 mBackImageUrl = backImageUrl;
693 mFilter = filter;
694 mEmptyViewConfig = emptyViewConfig;
695 mFlags = flags;
696
697 validate();
698 }
699
700 private void validate() {
701 if (mType == null) {
702 throw new IllegalArgumentException("Can't create ViewConfig with null type");
703 }
704
705 if (TextUtils.isEmpty(mDatasetId)) {
706 throw new IllegalArgumentException("Can't create ViewConfig with empty dataset ID");
707 }
708
709 if (mItemType == null) {
710 throw new IllegalArgumentException("Can't create ViewConfig with null item type");
711 }
712
713 if (mItemHandler == null) {
714 throw new IllegalArgumentException("Can't create ViewConfig with null item handler");
715 }
716
717 if (mFlags == null) {
718 throw new IllegalArgumentException("Can't create ViewConfig with null flags");
719 }
720 }
721
722 public int getIndex() {
723 return mIndex;
724 }
725
726 public ViewType getType() {
727 return mType;
728 }
729
730 public String getDatasetId() {
731 return mDatasetId;
732 }
733
734 public ItemType getItemType() {
735 return mItemType;
736 }
737
738 public ItemHandler getItemHandler() {
739 return mItemHandler;
740 }
741
742 public String getBackImageUrl() {
743 return mBackImageUrl;
744 }
745
746 public String getFilter() {
747 return mFilter;
748 }
749
750 public EmptyViewConfig getEmptyViewConfig() {
751 return mEmptyViewConfig;
752 }
753
754 public boolean isRefreshEnabled() {
755 return mFlags.contains(Flags.REFRESH_ENABLED);
756 }
757
758 public JSONObject toJSON() throws JSONException {
759 final JSONObject json = new JSONObject();
760
761 json.put(JSON_KEY_TYPE, mType.toString());
762 json.put(JSON_KEY_DATASET, mDatasetId);
763 json.put(JSON_KEY_ITEM_TYPE, mItemType.toString());
764 json.put(JSON_KEY_ITEM_HANDLER, mItemHandler.toString());
765
766 if (!TextUtils.isEmpty(mBackImageUrl)) {
767 json.put(JSON_KEY_BACK_IMAGE_URL, mBackImageUrl);
768 }
769
770 if (!TextUtils.isEmpty(mFilter)) {
771 json.put(JSON_KEY_FILTER, mFilter);
772 }
773
774 if (mEmptyViewConfig != null) {
775 json.put(JSON_KEY_EMPTY, mEmptyViewConfig.toJSON());
776 }
777
778 if (mFlags.contains(Flags.REFRESH_ENABLED)) {
779 json.put(JSON_KEY_REFRESH_ENABLED, true);
780 }
781
782 return json;
783 }
784
785 @Override
786 public int describeContents() {
787 return 0;
788 }
789
790 @Override
791 public void writeToParcel(Parcel dest, int flags) {
792 dest.writeInt(mIndex);
793 dest.writeParcelable(mType, 0);
794 dest.writeString(mDatasetId);
795 dest.writeParcelable(mItemType, 0);
796 dest.writeParcelable(mItemHandler, 0);
797 dest.writeString(mBackImageUrl);
798 dest.writeString(mFilter);
799 dest.writeParcelable(mEmptyViewConfig, 0);
800 dest.writeSerializable(mFlags);
801 }
802
803 public static final Creator<ViewConfig> CREATOR = new Creator<ViewConfig>() {
804 @Override
805 public ViewConfig createFromParcel(final Parcel in) {
806 return new ViewConfig(in);
807 }
808
809 @Override
810 public ViewConfig[] newArray(final int size) {
811 return new ViewConfig[size];
812 }
813 };
814 }
815
816 public static class EmptyViewConfig implements Parcelable {
817 private final String mText;
818 private final String mImageUrl;
819
820 private static final String JSON_KEY_TEXT = "text";
821 private static final String JSON_KEY_IMAGE_URL = "imageUrl";
822
823 public EmptyViewConfig(JSONObject json) throws JSONException, IllegalArgumentException {
824 mText = json.optString(JSON_KEY_TEXT, null);
825 mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null);
826 }
827
828 @SuppressWarnings("unchecked")
829 public EmptyViewConfig(Parcel in) {
830 mText = in.readString();
831 mImageUrl = in.readString();
832 }
833
834 public EmptyViewConfig(EmptyViewConfig emptyViewConfig) {
835 mText = emptyViewConfig.mText;
836 mImageUrl = emptyViewConfig.mImageUrl;
837 }
838
839 public EmptyViewConfig(String text, String imageUrl) {
840 mText = text;
841 mImageUrl = imageUrl;
842 }
843
844 public String getText() {
845 return mText;
846 }
847
848 public String getImageUrl() {
849 return mImageUrl;
850 }
851
852 public JSONObject toJSON() throws JSONException {
853 final JSONObject json = new JSONObject();
854
855 json.put(JSON_KEY_TEXT, mText);
856 json.put(JSON_KEY_IMAGE_URL, mImageUrl);
857
858 return json;
859 }
860
861 @Override
862 public int describeContents() {
863 return 0;
864 }
865
866 @Override
867 public void writeToParcel(Parcel dest, int flags) {
868 dest.writeString(mText);
869 dest.writeString(mImageUrl);
870 }
871
872 public static final Creator<EmptyViewConfig> CREATOR = new Creator<EmptyViewConfig>() {
873 @Override
874 public EmptyViewConfig createFromParcel(final Parcel in) {
875 return new EmptyViewConfig(in);
876 }
877
878 @Override
879 public EmptyViewConfig[] newArray(final int size) {
880 return new EmptyViewConfig[size];
881 }
882 };
883 }
884
885 public static class AuthConfig implements Parcelable {
886 private final String mMessageText;
887 private final String mButtonText;
888 private final String mImageUrl;
889
890 private static final String JSON_KEY_MESSAGE_TEXT = "messageText";
891 private static final String JSON_KEY_BUTTON_TEXT = "buttonText";
892 private static final String JSON_KEY_IMAGE_URL = "imageUrl";
893
894 public AuthConfig(JSONObject json) throws JSONException, IllegalArgumentException {
895 mMessageText = json.optString(JSON_KEY_MESSAGE_TEXT);
896 mButtonText = json.optString(JSON_KEY_BUTTON_TEXT);
897 mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null);
898 }
899
900 @SuppressWarnings("unchecked")
901 public AuthConfig(Parcel in) {
902 mMessageText = in.readString();
903 mButtonText = in.readString();
904 mImageUrl = in.readString();
905
906 validate();
907 }
908
909 public AuthConfig(AuthConfig authConfig) {
910 mMessageText = authConfig.mMessageText;
911 mButtonText = authConfig.mButtonText;
912 mImageUrl = authConfig.mImageUrl;
913
914 validate();
915 }
916
917 public AuthConfig(String messageText, String buttonText, String imageUrl) {
918 mMessageText = messageText;
919 mButtonText = buttonText;
920 mImageUrl = imageUrl;
921
922 validate();
923 }
924
925 private void validate() {
926 if (mMessageText == null) {
927 throw new IllegalArgumentException("Can't create AuthConfig with null message text");
928 }
929
930 if (mButtonText == null) {
931 throw new IllegalArgumentException("Can't create AuthConfig with null button text");
932 }
933 }
934
935 public String getMessageText() {
936 return mMessageText;
937 }
938
939 public String getButtonText() {
940 return mButtonText;
941 }
942
943 public String getImageUrl() {
944 return mImageUrl;
945 }
946
947 public JSONObject toJSON() throws JSONException {
948 final JSONObject json = new JSONObject();
949
950 json.put(JSON_KEY_MESSAGE_TEXT, mMessageText);
951 json.put(JSON_KEY_BUTTON_TEXT, mButtonText);
952 json.put(JSON_KEY_IMAGE_URL, mImageUrl);
953
954 return json;
955 }
956
957 @Override
958 public int describeContents() {
959 return 0;
960 }
961
962 @Override
963 public void writeToParcel(Parcel dest, int flags) {
964 dest.writeString(mMessageText);
965 dest.writeString(mButtonText);
966 dest.writeString(mImageUrl);
967 }
968
969 public static final Creator<AuthConfig> CREATOR = new Creator<AuthConfig>() {
970 @Override
971 public AuthConfig createFromParcel(final Parcel in) {
972 return new AuthConfig(in);
973 }
974
975 @Override
976 public AuthConfig[] newArray(final int size) {
977 return new AuthConfig[size];
978 }
979 };
980 }
981 /**
982 * Immutable representation of the current state of {@code HomeConfig}.
983 * This is what HomeConfig returns from a load() call and takes as
984 * input to save a new state.
985 *
986 * Users of {@code State} should use an {@code Iterator} to iterate
987 * through the contained {@code PanelConfig} instances.
988 *
989 * {@code State} is immutable i.e. you can't add, remove, or update
990 * contained elements directly. You have to use an {@code Editor} to
991 * change the state, which can be created through the {@code edit()}
992 * method.
993 */
994 public static class State implements Iterable<PanelConfig> {
995 private HomeConfig mHomeConfig;
996 private final List<PanelConfig> mPanelConfigs;
997 private final boolean mIsDefault;
998
999 State(List<PanelConfig> panelConfigs, boolean isDefault) {
1000 this(null, panelConfigs, isDefault);
1001 }
1002
1003 private State(HomeConfig homeConfig, List<PanelConfig> panelConfigs, boolean isDefault) {
1004 mHomeConfig = homeConfig;
1005 mPanelConfigs = Collections.unmodifiableList(panelConfigs);
1006 mIsDefault = isDefault;
1007 }
1008
1009 private void setHomeConfig(HomeConfig homeConfig) {
1010 if (mHomeConfig != null) {
1011 throw new IllegalStateException("Can't set HomeConfig more than once");
1012 }
1013
1014 mHomeConfig = homeConfig;
1015 }
1016
1017 @Override
1018 public Iterator<PanelConfig> iterator() {
1019 return mPanelConfigs.iterator();
1020 }
1021
1022 /**
1023 * Returns whether this {@code State} instance represents the default
1024 * {@code HomeConfig} configuration or not.
1025 */
1026 public boolean isDefault() {
1027 return mIsDefault;
1028 }
1029
1030 /**
1031 * Creates an {@code Editor} for this state.
1032 */
1033 public Editor edit() {
1034 return new Editor(mHomeConfig, this);
1035 }
1036 }
1037
1038 /**
1039 * {@code Editor} allows you to make changes to a {@code State}. You
1040 * can create {@code Editor} by calling {@code edit()} on the target
1041 * {@code State} instance.
1042 *
1043 * {@code Editor} works on a copy of the {@code State} that originated
1044 * it. This means that adding, removing, or updating panels in an
1045 * {@code Editor} will never change the {@code State} which you
1046 * created the {@code Editor} from. Calling {@code commit()} or
1047 * {@code apply()} will cause the new {@code State} instance to be
1048 * created and saved using the {@code HomeConfig} instance that
1049 * created the source {@code State}.
1050 *
1051 * {@code Editor} is *not* thread-safe. You can only make calls on it
1052 * from the thread where it was originally created. It will throw an
1053 * exception if you don't follow this invariant.
1054 */
1055 public static class Editor implements Iterable<PanelConfig> {
1056 private final HomeConfig mHomeConfig;
1057 private final Map<String, PanelConfig> mConfigMap;
1058 private final List<String> mConfigOrder;
1059 private final List<GeckoEvent> mEventQueue;
1060 private final Thread mOriginalThread;
1061
1062 private PanelConfig mDefaultPanel;
1063 private int mEnabledCount;
1064
1065 private boolean mHasChanged;
1066 private final boolean mIsFromDefault;
1067
1068 private Editor(HomeConfig homeConfig, State configState) {
1069 mHomeConfig = homeConfig;
1070 mOriginalThread = Thread.currentThread();
1071 mConfigMap = new HashMap<String, PanelConfig>();
1072 mConfigOrder = new LinkedList<String>();
1073 mEventQueue = new LinkedList<GeckoEvent>();
1074 mEnabledCount = 0;
1075
1076 mHasChanged = false;
1077 mIsFromDefault = configState.isDefault();
1078
1079 initFromState(configState);
1080 }
1081
1082 /**
1083 * Initialize the initial state of the editor from the given
1084 * {@sode State}. A HashMap is used to represent the list of
1085 * panels as it provides fast access, and a LinkedList is used to
1086 * keep track of order. We keep a reference to the default panel
1087 * and the number of enabled panels to avoid iterating through the
1088 * map every time we need those.
1089 *
1090 * @param configState The source State to load the editor from.
1091 */
1092 private void initFromState(State configState) {
1093 for (PanelConfig panelConfig : configState) {
1094 final PanelConfig panelCopy = new PanelConfig(panelConfig);
1095
1096 if (!panelCopy.isDisabled()) {
1097 mEnabledCount++;
1098 }
1099
1100 if (panelCopy.isDefault()) {
1101 if (mDefaultPanel == null) {
1102 mDefaultPanel = panelCopy;
1103 } else {
1104 throw new IllegalStateException("Multiple default panels in HomeConfig state");
1105 }
1106 }
1107
1108 final String panelId = panelConfig.getId();
1109 mConfigOrder.add(panelId);
1110 mConfigMap.put(panelId, panelCopy);
1111 }
1112
1113 // We should always have a defined default panel if there's
1114 // at least one enabled panel around.
1115 if (mEnabledCount > 0 && mDefaultPanel == null) {
1116 throw new IllegalStateException("Default panel in HomeConfig state is undefined");
1117 }
1118 }
1119
1120 private PanelConfig getPanelOrThrow(String panelId) {
1121 final PanelConfig panelConfig = mConfigMap.get(panelId);
1122 if (panelConfig == null) {
1123 throw new IllegalStateException("Tried to access non-existing panel: " + panelId);
1124 }
1125
1126 return panelConfig;
1127 }
1128
1129 private boolean isCurrentDefaultPanel(PanelConfig panelConfig) {
1130 if (mDefaultPanel == null) {
1131 return false;
1132 }
1133
1134 return mDefaultPanel.equals(panelConfig);
1135 }
1136
1137 private void findNewDefault() {
1138 // Pick the first panel that is neither disabled nor currently
1139 // set as default.
1140 for (PanelConfig panelConfig : mConfigMap.values()) {
1141 if (!panelConfig.isDefault() && !panelConfig.isDisabled()) {
1142 setDefault(panelConfig.getId());
1143 return;
1144 }
1145 }
1146
1147 mDefaultPanel = null;
1148 }
1149
1150 /**
1151 * Makes an ordered list of PanelConfigs that can be references
1152 * or deep copied objects.
1153 *
1154 * @param deepCopy true to make deep-copied objects
1155 * @return ordered List of PanelConfigs
1156 */
1157 private List<PanelConfig> makeOrderedCopy(boolean deepCopy) {
1158 final List<PanelConfig> copiedList = new ArrayList<PanelConfig>(mConfigOrder.size());
1159 for (String panelId : mConfigOrder) {
1160 PanelConfig panelConfig = mConfigMap.get(panelId);
1161 if (deepCopy) {
1162 panelConfig = new PanelConfig(panelConfig);
1163 }
1164 copiedList.add(panelConfig);
1165 }
1166
1167 return copiedList;
1168 }
1169
1170 private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) {
1171 if (panelConfig.isDisabled() == disabled) {
1172 return;
1173 }
1174
1175 panelConfig.setIsDisabled(disabled);
1176 mEnabledCount += (disabled ? -1 : 1);
1177 }
1178
1179 /**
1180 * Gets the ID of the current default panel.
1181 */
1182 public String getDefaultPanelId() {
1183 ThreadUtils.assertOnThread(mOriginalThread);
1184
1185 if (mDefaultPanel == null) {
1186 return null;
1187 }
1188
1189 return mDefaultPanel.getId();
1190 }
1191
1192 /**
1193 * Set a new default panel.
1194 *
1195 * @param panelId the ID of the new default panel.
1196 */
1197 public void setDefault(String panelId) {
1198 ThreadUtils.assertOnThread(mOriginalThread);
1199
1200 final PanelConfig panelConfig = getPanelOrThrow(panelId);
1201 if (isCurrentDefaultPanel(panelConfig)) {
1202 return;
1203 }
1204
1205 if (mDefaultPanel != null) {
1206 mDefaultPanel.setIsDefault(false);
1207 }
1208
1209 panelConfig.setIsDefault(true);
1210 setPanelIsDisabled(panelConfig, false);
1211
1212 mDefaultPanel = panelConfig;
1213 mHasChanged = true;
1214 }
1215
1216 /**
1217 * Toggles disabled state for a panel.
1218 *
1219 * @param panelId the ID of the target panel.
1220 * @param disabled true to disable the panel.
1221 */
1222 public void setDisabled(String panelId, boolean disabled) {
1223 ThreadUtils.assertOnThread(mOriginalThread);
1224
1225 final PanelConfig panelConfig = getPanelOrThrow(panelId);
1226 if (panelConfig.isDisabled() == disabled) {
1227 return;
1228 }
1229
1230 setPanelIsDisabled(panelConfig, disabled);
1231
1232 if (disabled) {
1233 if (isCurrentDefaultPanel(panelConfig)) {
1234 panelConfig.setIsDefault(false);
1235 findNewDefault();
1236 }
1237 } else if (mEnabledCount == 1) {
1238 setDefault(panelId);
1239 }
1240
1241 mHasChanged = true;
1242 }
1243
1244 /**
1245 * Adds a new {@code PanelConfig}. It will do nothing if the
1246 * {@code Editor} already contains a panel with the same ID.
1247 *
1248 * @param panelConfig the {@code PanelConfig} instance to be added.
1249 * @return true if the item has been added.
1250 */
1251 public boolean install(PanelConfig panelConfig) {
1252 ThreadUtils.assertOnThread(mOriginalThread);
1253
1254 if (panelConfig == null) {
1255 throw new IllegalStateException("Can't install a null panel");
1256 }
1257
1258 if (!panelConfig.isDynamic()) {
1259 throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId());
1260 }
1261
1262 if (panelConfig.isDisabled()) {
1263 throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId());
1264 }
1265
1266 boolean installed = false;
1267
1268 final String id = panelConfig.getId();
1269 if (!mConfigMap.containsKey(id)) {
1270 mConfigMap.put(id, panelConfig);
1271 mConfigOrder.add(id);
1272
1273 mEnabledCount++;
1274 if (mEnabledCount == 1 || panelConfig.isDefault()) {
1275 setDefault(panelConfig.getId());
1276 }
1277
1278 installed = true;
1279
1280 // Add an event to the queue if a new panel is sucessfully installed.
1281 mEventQueue.add(GeckoEvent.createBroadcastEvent("HomePanels:Installed", panelConfig.getId()));
1282 }
1283
1284 mHasChanged = true;
1285 return installed;
1286 }
1287
1288 /**
1289 * Removes an existing panel.
1290 *
1291 * @return true if the item has been removed.
1292 */
1293 public boolean uninstall(String panelId) {
1294 ThreadUtils.assertOnThread(mOriginalThread);
1295
1296 final PanelConfig panelConfig = mConfigMap.get(panelId);
1297 if (panelConfig == null) {
1298 return false;
1299 }
1300
1301 if (!panelConfig.isDynamic()) {
1302 throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId());
1303 }
1304
1305 mConfigMap.remove(panelId);
1306 mConfigOrder.remove(panelId);
1307
1308 if (!panelConfig.isDisabled()) {
1309 mEnabledCount--;
1310 }
1311
1312 if (isCurrentDefaultPanel(panelConfig)) {
1313 findNewDefault();
1314 }
1315
1316 // Add an event to the queue if a panel is succesfully uninstalled.
1317 mEventQueue.add(GeckoEvent.createBroadcastEvent("HomePanels:Uninstalled", panelId));
1318
1319 mHasChanged = true;
1320 return true;
1321 }
1322
1323 /**
1324 * Moves panel associated with panelId to the specified position.
1325 *
1326 * @param panelId Id of panel
1327 * @param destIndex Destination position
1328 * @return true if move succeeded
1329 */
1330 public boolean moveTo(String panelId, int destIndex) {
1331 ThreadUtils.assertOnThread(mOriginalThread);
1332
1333 if (!mConfigOrder.contains(panelId)) {
1334 return false;
1335 }
1336
1337 mConfigOrder.remove(panelId);
1338 mConfigOrder.add(destIndex, panelId);
1339 mHasChanged = true;
1340
1341 return true;
1342 }
1343
1344 /**
1345 * Replaces an existing panel with a new {@code PanelConfig} instance.
1346 *
1347 * @return true if the item has been updated.
1348 */
1349 public boolean update(PanelConfig panelConfig) {
1350 ThreadUtils.assertOnThread(mOriginalThread);
1351
1352 if (panelConfig == null) {
1353 throw new IllegalStateException("Can't update a null panel");
1354 }
1355
1356 boolean updated = false;
1357
1358 final String id = panelConfig.getId();
1359 if (mConfigMap.containsKey(id)) {
1360 final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig);
1361
1362 // The disabled and default states can't never be
1363 // changed by an update operation.
1364 panelConfig.setIsDefault(oldPanelConfig.isDefault());
1365 panelConfig.setIsDisabled(oldPanelConfig.isDisabled());
1366
1367 updated = true;
1368 }
1369
1370 mHasChanged = true;
1371 return updated;
1372 }
1373
1374 /**
1375 * Saves the current {@code Editor} state asynchronously in the
1376 * background thread.
1377 *
1378 * @return the resulting {@code State} instance.
1379 */
1380 public State apply() {
1381 ThreadUtils.assertOnThread(mOriginalThread);
1382
1383 // We're about to save the current state in the background thread
1384 // so we should use a deep copy of the PanelConfig instances to
1385 // avoid saving corrupted state.
1386 final State newConfigState =
1387 new State(mHomeConfig, makeOrderedCopy(true), isDefault());
1388
1389 // Copy the event queue to a new list, so that we only modify mEventQueue on
1390 // the original thread where it was created.
1391 final LinkedList<GeckoEvent> eventQueueCopy = new LinkedList<GeckoEvent>(mEventQueue);
1392 mEventQueue.clear();
1393
1394 ThreadUtils.getBackgroundHandler().post(new Runnable() {
1395 @Override
1396 public void run() {
1397 mHomeConfig.save(newConfigState);
1398
1399 // Send pending events after the new config is saved.
1400 sendEventsToGecko(eventQueueCopy);
1401 }
1402 });
1403
1404 return newConfigState;
1405 }
1406
1407 /**
1408 * Saves the current {@code Editor} state synchronously in the
1409 * current thread.
1410 *
1411 * @return the resulting {@code State} instance.
1412 */
1413 public State commit() {
1414 ThreadUtils.assertOnThread(mOriginalThread);
1415
1416 final State newConfigState =
1417 new State(mHomeConfig, makeOrderedCopy(false), isDefault());
1418
1419 // This is a synchronous blocking operation, hence no
1420 // need to deep copy the current PanelConfig instances.
1421 mHomeConfig.save(newConfigState);
1422
1423 // Send pending events after the new config is saved.
1424 sendEventsToGecko(mEventQueue);
1425 mEventQueue.clear();
1426
1427 return newConfigState;
1428 }
1429
1430 /**
1431 * Returns whether the {@code Editor} represents the default
1432 * {@code HomeConfig} configuration without any unsaved changes.
1433 */
1434 public boolean isDefault() {
1435 ThreadUtils.assertOnThread(mOriginalThread);
1436
1437 return (!mHasChanged && mIsFromDefault);
1438 }
1439
1440 public boolean isEmpty() {
1441 return mConfigMap.isEmpty();
1442 }
1443
1444 private void sendEventsToGecko(List<GeckoEvent> events) {
1445 for (GeckoEvent e : events) {
1446 GeckoAppShell.sendEventToGecko(e);
1447 }
1448 }
1449
1450 private class EditorIterator implements Iterator<PanelConfig> {
1451 private final Iterator<String> mOrderIterator;
1452
1453 public EditorIterator() {
1454 mOrderIterator = mConfigOrder.iterator();
1455 }
1456
1457 @Override
1458 public boolean hasNext() {
1459 return mOrderIterator.hasNext();
1460 }
1461
1462 @Override
1463 public PanelConfig next() {
1464 final String panelId = mOrderIterator.next();
1465 return mConfigMap.get(panelId);
1466 }
1467
1468 @Override
1469 public void remove() {
1470 throw new UnsupportedOperationException("Can't 'remove' from on Editor iterator.");
1471 }
1472 }
1473
1474 @Override
1475 public Iterator<PanelConfig> iterator() {
1476 ThreadUtils.assertOnThread(mOriginalThread);
1477
1478 return new EditorIterator();
1479 }
1480 }
1481
1482 public interface OnReloadListener {
1483 public void onReload();
1484 }
1485
1486 public interface HomeConfigBackend {
1487 public State load();
1488 public void save(State configState);
1489 public String getLocale();
1490 public void setOnReloadListener(OnReloadListener listener);
1491 }
1492
1493 // UUIDs used to create PanelConfigs for default built-in panels
1494 private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e";
1495 private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907";
1496 private static final String READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
1497 private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
1498
1499 private final HomeConfigBackend mBackend;
1500
1501 public HomeConfig(HomeConfigBackend backend) {
1502 mBackend = backend;
1503 }
1504
1505 public State load() {
1506 final State configState = mBackend.load();
1507 configState.setHomeConfig(this);
1508
1509 return configState;
1510 }
1511
1512 public String getLocale() {
1513 return mBackend.getLocale();
1514 }
1515
1516 public void save(State configState) {
1517 mBackend.save(configState);
1518 }
1519
1520 public void setOnReloadListener(OnReloadListener listener) {
1521 mBackend.setOnReloadListener(listener);
1522 }
1523
1524 public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) {
1525 return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class));
1526 }
1527
1528 public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType, EnumSet<PanelConfig.Flags> flags) {
1529 int titleId = 0;
1530 String id = null;
1531
1532 switch(panelType) {
1533 case TOP_SITES:
1534 titleId = R.string.home_top_sites_title;
1535 id = TOP_SITES_PANEL_ID;
1536 break;
1537
1538 case BOOKMARKS:
1539 titleId = R.string.bookmarks_title;
1540 id = BOOKMARKS_PANEL_ID;
1541 break;
1542
1543 case HISTORY:
1544 titleId = R.string.home_history_title;
1545 id = HISTORY_PANEL_ID;
1546 break;
1547
1548 case READING_LIST:
1549 titleId = R.string.reading_list_title;
1550 id = READING_LIST_PANEL_ID;
1551 break;
1552
1553 case DYNAMIC:
1554 throw new IllegalArgumentException("createBuiltinPanelConfig() is only for built-in panels");
1555 }
1556
1557 return new PanelConfig(panelType, context.getString(titleId), id, flags);
1558 }
1559
1560 public static HomeConfig getDefault(Context context) {
1561 return new HomeConfig(new HomeConfigPrefsBackend(context));
1562 }
1563 }

mercurial