toBeLoaded = new ArrayList<>();
21 |
22 | BundleService(BundleServiceRunner runner, MortarScope scope) {
23 | this.runner = runner;
24 | this.scope = scope;
25 | scopeBundle = findScopeBundle(runner.rootBundle);
26 | }
27 |
28 | public static BundleService getBundleService(Context context) {
29 | BundleServiceRunner runner = BundleServiceRunner.getBundleServiceRunner(context);
30 | if (runner == null) {
31 | throw new IllegalStateException(
32 | "You forgot to set up a " + BundleServiceRunner.class.getName() + " in your activity");
33 | }
34 | return runner.requireBundleService(MortarScope.getScope(context));
35 | }
36 |
37 | public static BundleService getBundleService(MortarScope scope) {
38 | BundleServiceRunner runner = BundleServiceRunner.getBundleServiceRunner(scope);
39 | if (runner == null) {
40 | throw new IllegalStateException(
41 | "You forgot to set up a " + BundleServiceRunner.class.getName() + " in your activity");
42 | }
43 | return runner.requireBundleService(scope);
44 | }
45 |
46 | /**
47 | * Registers {@link Bundler} instances with this service. See that interface for details.
48 | */
49 | public void register(Bundler bundler) {
50 | if (bundler == null) throw new NullPointerException("Cannot register null bundler.");
51 |
52 | if (runner.state == BundleServiceRunner.State.SAVING) {
53 | throw new IllegalStateException("Cannot register during onSave");
54 | }
55 |
56 | if (bundlers.add(bundler)) bundler.onEnterScope(scope);
57 | String mortarBundleKey = bundler.getMortarBundleKey();
58 | if (mortarBundleKey == null || mortarBundleKey.trim().equals("")) {
59 | throw new IllegalArgumentException(format("%s has null or empty bundle key", bundler));
60 | }
61 |
62 | switch (runner.state) {
63 | case IDLE:
64 | toBeLoaded.add(bundler);
65 | runner.servicesToBeLoaded.add(this);
66 | runner.finishLoading();
67 | break;
68 | case LOADING:
69 | if (!toBeLoaded.contains(bundler)) {
70 | toBeLoaded.add(bundler);
71 | runner.servicesToBeLoaded.add(this);
72 | }
73 | break;
74 |
75 | default:
76 | throw new AssertionError("Unexpected state " + runner.state);
77 | }
78 | }
79 |
80 | void init() {
81 | scope.register(new Scoped() {
82 | @Override public void onEnterScope(MortarScope scope) {
83 | runner.scopedServices.put(runner.bundleKey(scope), BundleService.this);
84 | }
85 |
86 | @Override public void onExitScope() {
87 | if (runner.rootBundle != null) runner.rootBundle.remove(runner.bundleKey(scope));
88 | for (Bundler b : bundlers) b.onExitScope();
89 | runner.scopedServices.remove(runner.bundleKey(scope));
90 | runner.servicesToBeLoaded.remove(BundleService.this);
91 | }
92 | });
93 | }
94 |
95 | boolean needsLoading() {
96 | return !toBeLoaded.isEmpty();
97 | }
98 |
99 | void loadOne() {
100 | if (toBeLoaded.isEmpty()) return;
101 |
102 | Bundler next = toBeLoaded.remove(0);
103 | Bundle leafBundle =
104 | scopeBundle == null ? null : scopeBundle.getBundle(next.getMortarBundleKey());
105 | next.onLoad(leafBundle);
106 | }
107 |
108 | /** @return true if we have clients that now need to be loaded */
109 | boolean updateScopedBundleOnCreate(Bundle rootBundle) {
110 | scopeBundle = findScopeBundle(rootBundle);
111 | toBeLoaded.addAll(bundlers);
112 | return !toBeLoaded.isEmpty();
113 | }
114 |
115 | private Bundle findScopeBundle(Bundle root) {
116 | return root == null ? null : root.getBundle(runner.bundleKey(scope));
117 | }
118 |
119 | void saveToRootBundle(Bundle rootBundle) {
120 | String key = runner.bundleKey(scope);
121 | scopeBundle = rootBundle.getBundle(key);
122 |
123 | if (scopeBundle == null) {
124 | scopeBundle = new Bundle();
125 | rootBundle.putBundle(key, scopeBundle);
126 | }
127 |
128 | for (Bundler bundler : bundlers) {
129 | Bundle childBundle = scopeBundle.getBundle(bundler.getMortarBundleKey());
130 | if (childBundle == null) {
131 | childBundle = new Bundle();
132 | scopeBundle.putBundle(bundler.getMortarBundleKey(), childBundle);
133 | }
134 |
135 | bundler.onSave(childBundle);
136 |
137 | // Short circuit if the scope was destroyed by the save call.
138 | if (scope.isDestroyed()) return;
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/mortar/src/main/java/mortar/bundler/BundleServiceComparator.java:
--------------------------------------------------------------------------------
1 | package mortar.bundler;
2 |
3 | import java.util.Comparator;
4 | import mortar.MortarScope;
5 |
6 | class BundleServiceComparator implements Comparator {
7 | @Override public int compare(BundleService left, BundleService right) {
8 | String[] leftPath = left.scope.getPath().split(MortarScope.DIVIDER);
9 | String[] rightPath = right.scope.getPath().split(MortarScope.DIVIDER);
10 |
11 | if (leftPath.length != rightPath.length) {
12 | return leftPath.length < rightPath.length ? -1 : 1;
13 | }
14 |
15 | int segments = leftPath.length;
16 | for (int i = 0; i < segments; i++) {
17 | int result = leftPath[i].compareTo(rightPath[i]);
18 | if (result != 0) return result;
19 | }
20 |
21 | return 0;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/mortar/src/main/java/mortar/bundler/BundleServiceRunner.java:
--------------------------------------------------------------------------------
1 | package mortar.bundler;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 | import java.util.ArrayList;
6 | import java.util.LinkedHashMap;
7 | import java.util.List;
8 | import java.util.Map;
9 | import java.util.NavigableSet;
10 | import java.util.TreeSet;
11 | import mortar.MortarScope;
12 | import mortar.Presenter;
13 | import mortar.Scoped;
14 |
15 | public class BundleServiceRunner implements Scoped {
16 | public static final String SERVICE_NAME = BundleServiceRunner.class.getName();
17 |
18 | public static BundleServiceRunner getBundleServiceRunner(Context context) {
19 | return (BundleServiceRunner) context.getSystemService(SERVICE_NAME);
20 | }
21 |
22 | public static BundleServiceRunner getBundleServiceRunner(MortarScope scope) {
23 | return scope.getService(SERVICE_NAME);
24 | }
25 |
26 | final Map scopedServices = new LinkedHashMap<>();
27 | final NavigableSet servicesToBeLoaded =
28 | new TreeSet<>(new BundleServiceComparator());
29 |
30 | Bundle rootBundle;
31 |
32 | enum State {
33 | IDLE, LOADING, SAVING
34 | }
35 |
36 | State state = State.IDLE;
37 |
38 | private String rootScopePath;
39 |
40 | BundleService requireBundleService(MortarScope scope) {
41 | BundleService service = scopedServices.get(bundleKey(scope));
42 | if (service == null) {
43 | service = new BundleService(this, scope);
44 | service.init();
45 | }
46 | return service;
47 | }
48 |
49 | @Override public void onEnterScope(MortarScope scope) {
50 | if (rootScopePath != null) throw new IllegalStateException("Cannot double register");
51 | rootScopePath = scope.getPath();
52 | }
53 |
54 | @Override public void onExitScope() {
55 | // Nothing to do.
56 | }
57 |
58 | /**
59 | * To be called from the host {@link android.app.Activity}'s {@link
60 | * android.app.Activity#onCreate}. Calls the registered {@link Bundler}'s {@link Bundler#onLoad}
61 | * methods. To avoid redundant calls to {@link Presenter#onLoad} it's best to call this before
62 | * {@link android.app.Activity#setContentView}.
63 | */
64 | public void onCreate(Bundle savedInstanceState) {
65 | rootBundle = savedInstanceState;
66 |
67 | for (Map.Entry entry : scopedServices.entrySet()) {
68 | BundleService scopedService = entry.getValue();
69 | if (scopedService.updateScopedBundleOnCreate(rootBundle)) {
70 | servicesToBeLoaded.add(scopedService);
71 | }
72 | }
73 | finishLoading();
74 | }
75 |
76 | /**
77 | * To be called from the host {@link android.app.Activity}'s {@link
78 | * android.app.Activity#onSaveInstanceState}. Calls the registrants' {@link Bundler#onSave}
79 | * methods.
80 | */
81 | public void onSaveInstanceState(Bundle outState) {
82 | if (state != State.IDLE) {
83 | throw new IllegalStateException("Cannot handle onSaveInstanceState while " + state);
84 | }
85 | rootBundle = outState;
86 |
87 | state = State.SAVING;
88 |
89 | // Make a dwindling copy of the services, in case one is deleted as a side effect
90 | // of another's onSave.
91 | List> servicesToBeSaved =
92 | new ArrayList<>(scopedServices.entrySet());
93 |
94 | while (!servicesToBeSaved.isEmpty()) {
95 | Map.Entry entry = servicesToBeSaved.remove(0);
96 | if (scopedServices.containsKey(entry.getKey())) entry.getValue().saveToRootBundle(rootBundle);
97 | }
98 |
99 | state = State.IDLE;
100 | }
101 |
102 | void finishLoading() {
103 | if (state != State.IDLE) throw new AssertionError("Unexpected state " + state);
104 | state = State.LOADING;
105 |
106 | while (!servicesToBeLoaded.isEmpty()) {
107 | BundleService next = servicesToBeLoaded.first();
108 | next.loadOne();
109 | if (!next.needsLoading()) servicesToBeLoaded.remove(next);
110 | }
111 |
112 | state = State.IDLE;
113 | }
114 |
115 | String bundleKey(MortarScope scope) {
116 | if (rootScopePath == null) throw new IllegalStateException("Was this service not registered?");
117 | String path = scope.getPath();
118 | if (!path.startsWith(rootScopePath)) {
119 | throw new IllegalArgumentException(String.format("\"%s\" is not under \"%s\"", scope,
120 | rootScopePath));
121 | }
122 |
123 | return path.substring(rootScopePath.length());
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/mortar/src/main/java/mortar/bundler/Bundler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Square Inc.
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 | package mortar.bundler;
17 |
18 | import android.os.Bundle;
19 | import mortar.MortarScope;
20 |
21 | /** Implemented by objects that want to persist via the bundle. */
22 | public interface Bundler {
23 | /**
24 | * Like {@link mortar.Scoped#onEnterScope}, called synchronously when a bundler
25 | * is {@link BundleService#register registered} with a {@link BundleService}.
26 | */
27 | void onEnterScope(MortarScope scope);
28 |
29 | /**
30 | * The key that will identify the bundles passed to this instance via {@link #onLoad}
31 | * and {@link #onSave}.
32 | */
33 | String getMortarBundleKey();
34 |
35 | /**
36 | * Called when this object is {@link BundleService#register registered}, and each time
37 | * {@link BundleServiceRunner#onCreate} is called (e.g. after a configuration change like
38 | * rotation, or after the app process is respawned). Callers should assume that the initial
39 | * call to this method is made asynchronously, but be prepared for a synchronous call.
40 | *
41 | * Note that receivers are likely to outlive multiple activity instances, and so receive
42 | * multiple calls of this method. Implementations should be prepared to ignore saved state if
43 | * they are already initialized.
44 | *
45 | * @param savedInstanceState the state written by the most recent call to {@link #onSave}, or
46 | * null if that has never happened.
47 | */
48 | void onLoad(Bundle savedInstanceState);
49 |
50 | /**
51 | * Called from the {@link BundleServiceRunner#onSaveInstanceState}, to allow the receiver
52 | * to save state before the process is killed. Note that receivers are likely to outlive multiple
53 | * activity instances, and so receive multiple calls of this method. Any state required to revive
54 | * a new instance of the receiver in a new process should be written out each time, as there is
55 | * no way to know if the app is about to hibernate.
56 | *
57 | * @param outState a bundle to write any state that needs to be restored if the plugin is
58 | * revived
59 | */
60 | void onSave(Bundle outState);
61 |
62 | void onExitScope();
63 | }
64 |
--------------------------------------------------------------------------------
/mortar/src/test/java/mortar/MortarScopeDevHelperTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Square Inc.
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 | package mortar;
17 |
18 | import org.junit.Test;
19 |
20 | import static mortar.MortarScopeDevHelper.scopeHierarchyToString;
21 | import static org.fest.assertions.api.Assertions.assertThat;
22 |
23 | public class MortarScopeDevHelperTest {
24 | private static final char BLANK = '\u00a0';
25 |
26 | @Test public void nestedScopeHierarchyToString() {
27 | MortarScope root = MortarScope.buildRootScope().build("Root");
28 | root.buildChild().build("Cadet");
29 |
30 | MortarScope colonel = root.buildChild().build("Colonel");
31 | colonel.buildChild().build("ElderColonel");
32 | colonel.buildChild().build("ZeElderColonel");
33 |
34 | MortarScope elder = root.buildChild().build("Elder");
35 | elder.buildChild().build("ElderCadet");
36 | elder.buildChild().build("ZeElderCadet");
37 | elder.buildChild().build("ElderElder");
38 | elder.buildChild().build("AnElderCadet");
39 |
40 | String hierarchy = scopeHierarchyToString(root);
41 | assertThat(hierarchy).isEqualTo("" //
42 | + "Mortar Hierarchy:\n" //
43 | + BLANK + "SCOPE Root\n" //
44 | + BLANK + "+-SCOPE Cadet\n" //
45 | + BLANK + "+-SCOPE Colonel\n" //
46 | + BLANK + "| +-SCOPE ElderColonel\n" //
47 | + BLANK + "| `-SCOPE ZeElderColonel\n" //
48 | + BLANK + "`-SCOPE Elder\n" //
49 | + BLANK + " +-SCOPE AnElderCadet\n" //
50 | + BLANK + " +-SCOPE ElderCadet\n" //
51 | + BLANK + " +-SCOPE ElderElder\n" //
52 | + BLANK + " `-SCOPE ZeElderCadet\n" //
53 | );
54 | }
55 |
56 | @Test public void startsFromMortarScope() {
57 | MortarScope root = MortarScope.buildRootScope().build("Root");
58 | MortarScope child = root.buildChild().build("Child");
59 |
60 | String hierarchy = scopeHierarchyToString(child);
61 |
62 | assertThat(hierarchy).isEqualTo("" //
63 | + "Mortar Hierarchy:\n" //
64 | + BLANK + "SCOPE Root\n" //
65 | + BLANK + "`-SCOPE Child\n" //
66 | );
67 | }
68 |
69 | @Test public void noSpaceAtLineBeginnings() {
70 | MortarScope root = MortarScope.buildRootScope().build("Root");
71 | MortarScope child = root.buildChild().build("Child");
72 | child.buildChild().build("Grand Child");
73 |
74 | String hierarchy = scopeHierarchyToString(root);
75 |
76 | assertThat(hierarchy).isEqualTo("" //
77 | + "Mortar Hierarchy:\n" //
78 | + BLANK + "SCOPE Root\n" //
79 | + BLANK + "`-SCOPE Child\n" //
80 | + BLANK + " `-SCOPE Grand Child\n" //
81 | );
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/mortar/src/test/java/mortar/PopupPresenterTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014 Square Inc.
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 | package mortar;
17 |
18 | import android.content.Context;
19 | import android.os.Bundle;
20 | import android.os.Parcelable;
21 | import mortar.bundler.BundleServiceRunner;
22 | import org.junit.Before;
23 | import org.junit.Test;
24 | import org.junit.runner.RunWith;
25 | import org.mockito.Mock;
26 | import org.mockito.invocation.InvocationOnMock;
27 | import org.mockito.stubbing.Answer;
28 | import org.robolectric.RobolectricTestRunner;
29 | import org.robolectric.annotation.Config;
30 |
31 | import static mortar.bundler.BundleServiceRunner.getBundleServiceRunner;
32 | import static org.fest.assertions.api.Assertions.assertThat;
33 | import static org.mockito.Matchers.any;
34 | import static org.mockito.Matchers.anyBoolean;
35 | import static org.mockito.Matchers.anyString;
36 | import static org.mockito.Matchers.eq;
37 | import static org.mockito.Matchers.same;
38 | import static org.mockito.Mockito.mock;
39 | import static org.mockito.Mockito.never;
40 | import static org.mockito.Mockito.verify;
41 | import static org.mockito.Mockito.when;
42 | import static org.mockito.MockitoAnnotations.initMocks;
43 |
44 | // Robolectric allows us to use Bundles.
45 | @RunWith(RobolectricTestRunner.class)
46 | @Config(manifest = Config.NONE)
47 | public class PopupPresenterTest {
48 |
49 | static class TestPopupPresenter extends PopupPresenter {
50 | String result;
51 |
52 | TestPopupPresenter() {
53 | }
54 |
55 | TestPopupPresenter(String customStateKey) {
56 | super(customStateKey);
57 | }
58 |
59 | @Override protected void onPopupResult(String result) {
60 | this.result = result;
61 | }
62 | }
63 |
64 | static final boolean WITH_FLOURISH = true;
65 | static final boolean WITHOUT_FLOURISH = false;
66 |
67 | @Mock Popup view;
68 | @Mock Context context;
69 |
70 | MortarScope root;
71 | MortarScope activityScope;
72 | TestPopupPresenter presenter;
73 |
74 | @Before public void setUp() {
75 | initMocks(this);
76 | when(view.getContext()).thenReturn(context);
77 | when((context).getSystemService(anyString())).then(returnScopedService());
78 |
79 | newProcess();
80 | getBundleServiceRunner(activityScope).onCreate(null);
81 | presenter = new TestPopupPresenter();
82 | }
83 |
84 | /** Simulate a new proecess by creating brand new scope instances. */
85 | private void newProcess() {
86 | root = MortarScope.buildRootScope().build("Root");
87 | activityScope = root.buildChild()
88 | .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner())
89 | .build("activity");
90 | }
91 |
92 | private Answer