DynamicSentinelStrategy.java revision 9c92f46280cf3943701e75349833c68b584992e2
1f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin/* 2f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Copyright (C) 2013 DroidDriver committers 3f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * 4f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Licensed under the Apache License, Version 2.0 (the "License"); 5f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * you may not use this file except in compliance with the License. 6f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * You may obtain a copy of the License at 7f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * 8f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * http://www.apache.org/licenses/LICENSE-2.0 9f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * 10f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Unless required by applicable law or agreed to in writing, software 11f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * distributed under the License is distributed on an "AS IS" BASIS, 12f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * See the License for the specific language governing permissions and 14f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * limitations under the License. 15f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 16f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jinpackage com.google.android.droiddriver.scroll; 17f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 18f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jinimport android.util.Log; 19f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 20f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jinimport com.google.android.droiddriver.DroidDriver; 21f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jinimport com.google.android.droiddriver.UiElement; 22f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jinimport com.google.android.droiddriver.exceptions.ElementNotFoundException; 23dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jinimport com.google.android.droiddriver.finders.By; 24f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jinimport com.google.android.droiddriver.finders.Finder; 25b5194043e9f0a1319dc7251f829febab3c76e277Kevin Jinimport com.google.android.droiddriver.scroll.Direction.DirectionConverter; 2629d66eeee5d30f7db747cceeb84defec961b4125Kevin Jinimport com.google.android.droiddriver.scroll.Direction.PhysicalDirection; 27f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jinimport com.google.android.droiddriver.util.Logs; 28f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jinimport com.google.common.base.Objects; 29f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 30f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin/** 31f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Determines whether scrolling is possible by checking whether the sentinel 320319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * child is updated after scrolling. Use this when {@link UiElement#getChildren} 330319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * is not reliable. This can happen, for instance, when UiAutomationDriver is 340319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * used, which skips invisible children, or in the case of dynamic list, which 350319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * shows more items when scrolling beyond the end. 36f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 379c92f46280cf3943701e75349833c68b584992e2Kevin Jinpublic class DynamicSentinelStrategy extends BaseSentinelStrategy { 38f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 39f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 40f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Interface for determining whether sentinel is updated. 41f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 42f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public static interface IsUpdatedStrategy { 43f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 44f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Returns whether {@code newSentinel} is updated from {@code oldSentinel}. 45f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 46f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel); 47f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 48f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 49f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * {@inheritDoc} 50f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * 51f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * <p> 52f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * It is recommended that this method return a description to help 53f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * debugging. 54f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 55f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin @Override 56f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin String toString(); 57f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 58f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 59f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 60f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Determines whether the sentinel is updated by checking a single unique 610319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * String attribute of a descendant element of the sentinel (or itself). 62f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 63f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public static abstract class SingleStringUpdated implements IsUpdatedStrategy { 64f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin private final Finder uniqueStringFinder; 65f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 66f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 67f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * @param uniqueStringFinder a Finder relative to the sentinel that finds 680319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * its descendant or self which contains a unique String. 69f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 70f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public SingleStringUpdated(Finder uniqueStringFinder) { 71f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin this.uniqueStringFinder = uniqueStringFinder; 72f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 73f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 74f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 750319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * @param uniqueStringElement the descendant or self that contains the 760319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * unique String 77f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * @return the unique String 78f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 790319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin protected abstract String getUniqueString(UiElement uniqueStringElement); 80f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 81f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin private String getUniqueStringFromSentinel(UiElement sentinel) { 82f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin try { 83f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin return getUniqueString(uniqueStringFinder.find(sentinel)); 84f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } catch (ElementNotFoundException e) { 85f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin return null; 86f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 87f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 88f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 89f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin @Override 90f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) { 9170e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin // If the sentinel moved, scrolling has some effect. This is both an 9270e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin // optimization - getBounds is cheaper than find - and necessary in 9370e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin // certain cases, e.g. user is looking for a sibling of the unique string; 9470e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin // the scroll is close to the end therefore the unique string does not 9570e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin // change, but the target could be revealed. 9670e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin if (!newSentinel.getBounds().equals(oldSentinel.getBounds())) { 9770e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin return true; 9870e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin } 9970e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin 100f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin String newString = getUniqueStringFromSentinel(newSentinel); 101a5bb27d69e8501b7c8321b838646d0b8f6fa0d05Kevin Jin // A legitimate case for newString being null is when newSentinel is 102a5bb27d69e8501b7c8321b838646d0b8f6fa0d05Kevin Jin // partially shown. We return true to allow further scrolling. But program 1039c92f46280cf3943701e75349833c68b584992e2Kevin Jin // error could also cause this, e.g. a bad choice of Getter, which 104a5bb27d69e8501b7c8321b838646d0b8f6fa0d05Kevin Jin // results in unnecessary scroll actions that have no visual effect. This 105a5bb27d69e8501b7c8321b838646d0b8f6fa0d05Kevin Jin // log helps troubleshooting in the latter case. 106f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin if (newString == null) { 107a5bb27d69e8501b7c8321b838646d0b8f6fa0d05Kevin Jin Logs.logfmt(Log.WARN, "Unique String is null: sentinel=%s, uniqueStringFinder=%s", 108a5bb27d69e8501b7c8321b838646d0b8f6fa0d05Kevin Jin newSentinel, uniqueStringFinder); 109f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin return true; 110f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 111f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin if (newString.equals(getUniqueStringFromSentinel(oldSentinel))) { 112f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin Logs.log(Log.INFO, "Unique String is not updated: " + newString); 113f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin return false; 114f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 115f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin return true; 116f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 117f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 118f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin @Override 119f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public String toString() { 120f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin return Objects.toStringHelper(this).addValue(uniqueStringFinder).toString(); 121f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 122f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 123f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 124f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 1250319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * Determines whether the sentinel is updated by checking the text of a 1260319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * descendant element of the sentinel (or itself). 127f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 128f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public static class TextUpdated extends SingleStringUpdated { 129f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public TextUpdated(Finder uniqueStringFinder) { 130f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin super(uniqueStringFinder); 131f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 132f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 133f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin @Override 1340319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin protected String getUniqueString(UiElement uniqueStringElement) { 1350319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin return uniqueStringElement.getText(); 136f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 137f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 138f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 139f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 140f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Determines whether the sentinel is updated by checking the content 1410319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin * description of a descendant element of the sentinel (or itself). 142f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 143f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public static class ContentDescriptionUpdated extends SingleStringUpdated { 144f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public ContentDescriptionUpdated(Finder uniqueStringFinder) { 145f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin super(uniqueStringFinder); 146f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 147f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 148f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin @Override 1490319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin protected String getUniqueString(UiElement uniqueStringElement) { 1500319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin return uniqueStringElement.getContentDescription(); 151f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 152f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 153f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 154dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin /** 155dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin * Determines whether the sentinel is updated by checking the resource-id of a 156dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin * descendant element of the sentinel (often itself). This is useful when the 157dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin * children of the container are heterogeneous -- they don't have a common 158dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin * pattern to get a unique string. 159dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin */ 160dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin public static class ResourceIdUpdated extends SingleStringUpdated { 161dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin /** 162dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin * Uses the resource-id of the sentinel itself. 163dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin */ 164dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin public static final ResourceIdUpdated SELF = new ResourceIdUpdated(By.any()); 165dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin 166dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin public ResourceIdUpdated(Finder uniqueStringFinder) { 167dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin super(uniqueStringFinder); 168dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin } 169dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin 170dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin @Override 171dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin protected String getUniqueString(UiElement uniqueStringElement) { 172dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin return uniqueStringElement.getResourceId(); 173dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin } 174dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin } 175dfc316e1bfb37148c50947c46f5aaed5cb2e708aKevin Jin 176f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin private final IsUpdatedStrategy isUpdatedStrategy; 177f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 178f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 1799c92f46280cf3943701e75349833c68b584992e2Kevin Jin * Constructs with {@code Getter}s that decorate the given {@code Getter}s 1809c92f46280cf3943701e75349833c68b584992e2Kevin Jin * with {@link UiElement#VISIBLE}, and the given {@code isUpdatedStrategy} and 1819c92f46280cf3943701e75349833c68b584992e2Kevin Jin * {@code directionConverter}. Be careful with {@code Getter}s: the sentinel 1829c92f46280cf3943701e75349833c68b584992e2Kevin Jin * after each scroll should be unique. 183f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 1849c92f46280cf3943701e75349833c68b584992e2Kevin Jin public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter, 1859c92f46280cf3943701e75349833c68b584992e2Kevin Jin Getter forwardGetter, DirectionConverter directionConverter) { 1869c92f46280cf3943701e75349833c68b584992e2Kevin Jin super(new MorePredicateGetter(backwardGetter, UiElement.VISIBLE, "VISIBLE_"), 1879c92f46280cf3943701e75349833c68b584992e2Kevin Jin new MorePredicateGetter(forwardGetter, UiElement.VISIBLE, "VISIBLE_"), directionConverter); 188f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin this.isUpdatedStrategy = isUpdatedStrategy; 189f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 190f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 191f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 192b5194043e9f0a1319dc7251f829febab3c76e277Kevin Jin * Defaults to the standard {@link DirectionConverter}. 193f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 1949c92f46280cf3943701e75349833c68b584992e2Kevin Jin public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter, 1959c92f46280cf3943701e75349833c68b584992e2Kevin Jin Getter forwardGetter) { 1969c92f46280cf3943701e75349833c68b584992e2Kevin Jin this(isUpdatedStrategy, backwardGetter, forwardGetter, DirectionConverter.STANDARD_CONVERTER); 197f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 198f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 199f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin /** 200f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin * Defaults to LAST_CHILD_GETTER for forward scrolling, and the standard 201b5194043e9f0a1319dc7251f829febab3c76e277Kevin Jin * {@link DirectionConverter}. 202f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin */ 2039c92f46280cf3943701e75349833c68b584992e2Kevin Jin public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter) { 2049c92f46280cf3943701e75349833c68b584992e2Kevin Jin this(isUpdatedStrategy, backwardGetter, LAST_CHILD_GETTER, 205b5194043e9f0a1319dc7251f829febab3c76e277Kevin Jin DirectionConverter.STANDARD_CONVERTER); 206f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 207f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 208f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin @Override 2090319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) { 2100319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin UiElement oldSentinel = getSentinel(driver, containerFinder, direction); 2116f160bb942de53103e4f4ed54acaafe2da629fcfKevin Jin oldSentinel.getParent().scroll(direction); 2120319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin UiElement newSentinel = getSentinel(driver, containerFinder, direction); 213f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin return isUpdatedStrategy.isSentinelUpdated(newSentinel, oldSentinel); 214f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 215f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin 216f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin @Override 217f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin public String toString() { 218f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(), 219f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin isUpdatedStrategy); 220f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin } 221f9c6c5063b38b623679e47d7095cccddb0481319Kevin Jin} 222