blob: 16b4368536a3cb8e736453e2fcafd66e674d7b14 [file] [log] [blame]
Todd Kennedyb8a279e2015-11-18 09:59:47 -08001/*
2 * Copyright (C) 2015 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
17package com.android.server.pm;
18
Patrick Baumann9c4aa5c2018-02-28 14:08:29 -080019import android.annotation.AnyThread;
20import android.annotation.WorkerThread;
Todd Kennedy1fb34042017-03-01 13:56:58 -080021import android.app.IInstantAppResolver;
22import android.app.InstantAppResolverService;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080023import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.ServiceConnection;
Todd Kennedy1fb34042017-03-01 13:56:58 -080027import android.content.pm.InstantAppResolveInfo;
Tony Makf2645402017-05-12 16:11:43 +010028import android.os.Binder;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080029import android.os.Build;
30import android.os.Bundle;
Todd Kennedy01ad0c72016-11-11 15:33:12 -080031import android.os.Handler;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080032import android.os.IBinder;
Todd Kennedy16c3a3e2017-04-03 12:03:35 -070033import android.os.IBinder.DeathRecipient;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080034import android.os.IRemoteCallback;
35import android.os.RemoteException;
36import android.os.SystemClock;
37import android.os.UserHandle;
Todd Kennedy7cf918e2017-04-10 15:10:56 -070038import android.util.Slog;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080039import android.util.TimedRemoteCaller;
40
Todd Kennedy46b4f2b2017-04-21 12:20:03 -070041import com.android.internal.annotations.GuardedBy;
Patrick Baumann9c4aa5c2018-02-28 14:08:29 -080042import com.android.internal.os.BackgroundThread;
Jeff Sharkey850c83e2016-11-09 12:25:44 -070043
Todd Kennedyb8a279e2015-11-18 09:59:47 -080044import java.util.ArrayList;
45import java.util.List;
Todd Kennedy34f5b422017-05-03 16:38:44 -070046import java.util.NoSuchElementException;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080047import java.util.concurrent.TimeoutException;
48
49/**
Patrick Baumann43c97a02018-01-31 20:09:03 +000050 * Represents a remote instant app resolver. It is responsible for binding to the remote
Todd Kennedyb8a279e2015-11-18 09:59:47 -080051 * service and handling all interactions in a timely manner.
52 * @hide
53 */
Patrick Baumann43c97a02018-01-31 20:09:03 +000054final class InstantAppResolverConnection implements DeathRecipient {
Todd Kennedy7cf918e2017-04-10 15:10:56 -070055 private static final String TAG = "PackageManager";
Todd Kennedyb8a279e2015-11-18 09:59:47 -080056 // This is running in a critical section and the timeout must be sufficiently low
57 private static final long BIND_SERVICE_TIMEOUT_MS =
Jeff Sharkey5ab02432017-06-27 11:01:36 -060058 Build.IS_ENG ? 500 : 300;
Todd Kennedy46b4f2b2017-04-21 12:20:03 -070059 private static final long CALL_SERVICE_TIMEOUT_MS =
Jeff Sharkey5ab02432017-06-27 11:01:36 -060060 Build.IS_ENG ? 200 : 100;
Patrick Baumann43c97a02018-01-31 20:09:03 +000061 private static final boolean DEBUG_INSTANT = Build.IS_DEBUGGABLE;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080062
63 private final Object mLock = new Object();
Patrick Baumann43c97a02018-01-31 20:09:03 +000064 private final GetInstantAppResolveInfoCaller mGetInstantAppResolveInfoCaller =
65 new GetInstantAppResolveInfoCaller();
Todd Kennedyb8a279e2015-11-18 09:59:47 -080066 private final ServiceConnection mServiceConnection = new MyServiceConnection();
67 private final Context mContext;
68 /** Intent used to bind to the service */
69 private final Intent mIntent;
70
Snild Dolkowe2612eb2017-06-14 09:57:36 +020071 private static final int STATE_IDLE = 0; // no bind operation is ongoing
72 private static final int STATE_BINDING = 1; // someone is binding and waiting
73 private static final int STATE_PENDING = 2; // a bind is pending, but the caller is not waiting
Patrick Baumann9c4aa5c2018-02-28 14:08:29 -080074 private final Handler mBgHandler;
Snild Dolkowe2612eb2017-06-14 09:57:36 +020075
Todd Kennedy46b4f2b2017-04-21 12:20:03 -070076 @GuardedBy("mLock")
Snild Dolkowe2612eb2017-06-14 09:57:36 +020077 private int mBindState = STATE_IDLE;
Todd Kennedy46b4f2b2017-04-21 12:20:03 -070078 @GuardedBy("mLock")
Todd Kennedy1fb34042017-03-01 13:56:58 -080079 private IInstantAppResolver mRemoteInstance;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080080
Patrick Baumann43c97a02018-01-31 20:09:03 +000081 public InstantAppResolverConnection(
Todd Kennedy02a6b732017-04-05 14:24:58 -070082 Context context, ComponentName componentName, String action) {
Todd Kennedyb8a279e2015-11-18 09:59:47 -080083 mContext = context;
Todd Kennedy02a6b732017-04-05 14:24:58 -070084 mIntent = new Intent(action).setComponent(componentName);
Patrick Baumann9c4aa5c2018-02-28 14:08:29 -080085 mBgHandler = BackgroundThread.getHandler();
Todd Kennedyb8a279e2015-11-18 09:59:47 -080086 }
87
Patrick Baumann577d4022018-01-31 16:55:10 +000088 public final List<InstantAppResolveInfo> getInstantAppResolveInfoList(Intent sanitizedIntent,
89 int hashPrefix[], String token) throws ConnectionException {
Todd Kennedyb8a279e2015-11-18 09:59:47 -080090 throwIfCalledOnMainThread();
Todd Kennedy46b4f2b2017-04-21 12:20:03 -070091 IInstantAppResolver target = null;
Todd Kennedyb8a279e2015-11-18 09:59:47 -080092 try {
Todd Kennedybdf2a802017-05-08 16:09:42 -070093 try {
94 target = getRemoteInstanceLazy(token);
95 } catch (TimeoutException e) {
96 throw new ConnectionException(ConnectionException.FAILURE_BIND);
97 } catch (InterruptedException e) {
98 throw new ConnectionException(ConnectionException.FAILURE_INTERRUPTED);
99 }
100 try {
Patrick Baumann43c97a02018-01-31 20:09:03 +0000101 return mGetInstantAppResolveInfoCaller
102 .getInstantAppResolveInfoList(target, sanitizedIntent, hashPrefix, token);
Todd Kennedybdf2a802017-05-08 16:09:42 -0700103 } catch (TimeoutException e) {
Todd Kennedye6393c92017-05-16 15:47:01 -0700104 throw new ConnectionException(ConnectionException.FAILURE_CALL);
Todd Kennedybdf2a802017-05-08 16:09:42 -0700105 } catch (RemoteException ignore) {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700106 }
Todd Kennedye5195dd2016-10-19 15:29:19 -0700107 } finally {
108 synchronized (mLock) {
109 mLock.notifyAll();
110 }
111 }
112 return null;
113 }
114
Patrick Baumann577d4022018-01-31 16:55:10 +0000115 public final void getInstantAppIntentFilterList(Intent sanitizedIntent, int hashPrefix[],
116 String token, PhaseTwoCallback callback, Handler callbackHandler, final long startTime)
117 throws ConnectionException {
Todd Kennedy01ad0c72016-11-11 15:33:12 -0800118 final IRemoteCallback remoteCallback = new IRemoteCallback.Stub() {
119 @Override
120 public void sendResult(Bundle data) throws RemoteException {
Todd Kennedy1fb34042017-03-01 13:56:58 -0800121 final ArrayList<InstantAppResolveInfo> resolveList =
122 data.getParcelableArrayList(
123 InstantAppResolverService.EXTRA_RESOLVE_INFO);
Patrick Baumann577d4022018-01-31 16:55:10 +0000124 callbackHandler.post(() -> callback.onPhaseTwoResolved(resolveList, startTime));
Todd Kennedy01ad0c72016-11-11 15:33:12 -0800125 }
126 };
Todd Kennedye5195dd2016-10-19 15:29:19 -0700127 try {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700128 getRemoteInstanceLazy(token)
Patrick Baumann577d4022018-01-31 16:55:10 +0000129 .getInstantAppIntentFilterList(sanitizedIntent, hashPrefix, token,
130 remoteCallback);
Todd Kennedybdf2a802017-05-08 16:09:42 -0700131 } catch (TimeoutException e) {
132 throw new ConnectionException(ConnectionException.FAILURE_BIND);
133 } catch (InterruptedException e) {
134 throw new ConnectionException(ConnectionException.FAILURE_INTERRUPTED);
135 } catch (RemoteException ignore) {
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800136 }
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800137 }
138
Patrick Baumann9c4aa5c2018-02-28 14:08:29 -0800139 @WorkerThread
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700140 private IInstantAppResolver getRemoteInstanceLazy(String token)
Todd Kennedybdf2a802017-05-08 16:09:42 -0700141 throws ConnectionException, TimeoutException, InterruptedException {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200142 long binderToken = Binder.clearCallingIdentity();
143 try {
144 return bind(token);
145 } finally {
146 Binder.restoreCallingIdentity(binderToken);
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800147 }
148 }
149
Andreas Gampea36dc622018-02-05 17:19:22 -0800150 @GuardedBy("mLock")
Todd Kennedybdf2a802017-05-08 16:09:42 -0700151 private void waitForBindLocked(String token) throws TimeoutException, InterruptedException {
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800152 final long startMillis = SystemClock.uptimeMillis();
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200153 while (mBindState != STATE_IDLE) {
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800154 if (mRemoteInstance != null) {
155 break;
156 }
157 final long elapsedMillis = SystemClock.uptimeMillis() - startMillis;
158 final long remainingMillis = BIND_SERVICE_TIMEOUT_MS - elapsedMillis;
159 if (remainingMillis <= 0) {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700160 throw new TimeoutException("[" + token + "] Didn't bind to resolver in time!");
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800161 }
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700162 mLock.wait(remainingMillis);
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800163 }
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700164 }
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800165
Patrick Baumann9c4aa5c2018-02-28 14:08:29 -0800166 @WorkerThread
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200167 private IInstantAppResolver bind(String token)
Todd Kennedybdf2a802017-05-08 16:09:42 -0700168 throws ConnectionException, TimeoutException, InterruptedException {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200169 boolean doUnbind = false;
170 synchronized (mLock) {
171 if (mRemoteInstance != null) {
172 return mRemoteInstance;
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700173 }
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200174
175 if (mBindState == STATE_PENDING) {
176 // there is a pending bind, let's see if we can use it.
Patrick Baumann43c97a02018-01-31 20:09:03 +0000177 if (DEBUG_INSTANT) {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200178 Slog.i(TAG, "[" + token + "] Previous bind timed out; waiting for connection");
179 }
180 try {
181 waitForBindLocked(token);
182 if (mRemoteInstance != null) {
183 return mRemoteInstance;
184 }
185 } catch (TimeoutException e) {
186 // nope, we might have to try a rebind.
187 doUnbind = true;
188 }
189 }
190
191 if (mBindState == STATE_BINDING) {
192 // someone was binding when we called bind(), or they raced ahead while we were
193 // waiting in the PENDING case; wait for their result instead. Last chance!
Patrick Baumann43c97a02018-01-31 20:09:03 +0000194 if (DEBUG_INSTANT) {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200195 Slog.i(TAG, "[" + token + "] Another thread is binding; waiting for connection");
196 }
197 waitForBindLocked(token);
198 // if the other thread's bindService() returned false, we could still have null.
199 if (mRemoteInstance != null) {
200 return mRemoteInstance;
201 }
202 throw new ConnectionException(ConnectionException.FAILURE_BIND);
203 }
204 mBindState = STATE_BINDING; // our time to shine! :)
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700205 }
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200206
207 // only one thread can be here at a time (the one that set STATE_BINDING)
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700208 boolean wasBound = false;
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200209 IInstantAppResolver instance = null;
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700210 try {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200211 if (doUnbind) {
Patrick Baumann43c97a02018-01-31 20:09:03 +0000212 if (DEBUG_INSTANT) {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200213 Slog.i(TAG, "[" + token + "] Previous connection never established; rebinding");
214 }
215 mContext.unbindService(mServiceConnection);
216 }
Patrick Baumann43c97a02018-01-31 20:09:03 +0000217 if (DEBUG_INSTANT) {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200218 Slog.v(TAG, "[" + token + "] Binding to instant app resolver");
219 }
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700220 final int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
221 wasBound = mContext
222 .bindServiceAsUser(mIntent, mServiceConnection, flags, UserHandle.SYSTEM);
223 if (wasBound) {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200224 synchronized (mLock) {
225 waitForBindLocked(token);
226 instance = mRemoteInstance;
227 return instance;
228 }
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700229 } else {
230 Slog.w(TAG, "[" + token + "] Failed to bind to: " + mIntent);
Todd Kennedybdf2a802017-05-08 16:09:42 -0700231 throw new ConnectionException(ConnectionException.FAILURE_BIND);
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700232 }
233 } finally {
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200234 synchronized (mLock) {
235 if (wasBound && instance == null) {
236 mBindState = STATE_PENDING;
237 } else {
238 mBindState = STATE_IDLE;
239 }
240 mLock.notifyAll();
241 }
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700242 }
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800243 }
244
245 private void throwIfCalledOnMainThread() {
246 if (Thread.currentThread() == mContext.getMainLooper().getThread()) {
247 throw new RuntimeException("Cannot invoke on the main thread");
248 }
249 }
250
Patrick Baumann9c4aa5c2018-02-28 14:08:29 -0800251 @AnyThread
252 void optimisticBind() {
253 mBgHandler.post(() -> {
254 try {
255 if (bind("Optimistic Bind") != null && DEBUG_INSTANT) {
256 Slog.i(TAG, "Optimistic bind succeeded.");
257 }
258 } catch (ConnectionException | TimeoutException | InterruptedException e) {
259 Slog.e(TAG, "Optimistic bind failed.", e);
260 }
261 });
262 }
263
Todd Kennedy16c3a3e2017-04-03 12:03:35 -0700264 @Override
265 public void binderDied() {
Patrick Baumann43c97a02018-01-31 20:09:03 +0000266 if (DEBUG_INSTANT) {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700267 Slog.d(TAG, "Binder to instant app resolver died");
Todd Kennedy7cf918e2017-04-10 15:10:56 -0700268 }
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700269 synchronized (mLock) {
270 handleBinderDiedLocked();
271 }
Patrick Baumann9c4aa5c2018-02-28 14:08:29 -0800272 optimisticBind();
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700273 }
274
Andreas Gampea36dc622018-02-05 17:19:22 -0800275 @GuardedBy("mLock")
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700276 private void handleBinderDiedLocked() {
Todd Kennedy16c3a3e2017-04-03 12:03:35 -0700277 if (mRemoteInstance != null) {
Todd Kennedy34f5b422017-05-03 16:38:44 -0700278 try {
279 mRemoteInstance.asBinder().unlinkToDeath(this, 0 /*flags*/);
280 } catch (NoSuchElementException ignore) { }
Todd Kennedy16c3a3e2017-04-03 12:03:35 -0700281 }
282 mRemoteInstance = null;
Todd Kennedy16c3a3e2017-04-03 12:03:35 -0700283 }
284
Todd Kennedy01ad0c72016-11-11 15:33:12 -0800285 /**
286 * Asynchronous callback when results come back from ephemeral resolution phase two.
287 */
288 public abstract static class PhaseTwoCallback {
Todd Kennedy1fb34042017-03-01 13:56:58 -0800289 abstract void onPhaseTwoResolved(
Todd Kennedy50d946c12017-03-17 13:55:38 -0700290 List<InstantAppResolveInfo> instantAppResolveInfoList, long startTime);
Todd Kennedy01ad0c72016-11-11 15:33:12 -0800291 }
292
Todd Kennedybdf2a802017-05-08 16:09:42 -0700293 public static class ConnectionException extends Exception {
294 public static final int FAILURE_BIND = 1;
295 public static final int FAILURE_CALL = 2;
296 public static final int FAILURE_INTERRUPTED = 3;
297
298 public final int failure;
299 public ConnectionException(int _failure) {
300 failure = _failure;
301 }
302 }
303
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800304 private final class MyServiceConnection implements ServiceConnection {
305 @Override
306 public void onServiceConnected(ComponentName name, IBinder service) {
Patrick Baumann43c97a02018-01-31 20:09:03 +0000307 if (DEBUG_INSTANT) {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700308 Slog.d(TAG, "Connected to instant app resolver");
Todd Kennedy7cf918e2017-04-10 15:10:56 -0700309 }
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800310 synchronized (mLock) {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700311 mRemoteInstance = IInstantAppResolver.Stub.asInterface(service);
Snild Dolkowe2612eb2017-06-14 09:57:36 +0200312 if (mBindState == STATE_PENDING) {
313 mBindState = STATE_IDLE;
314 }
Todd Kennedy16c3a3e2017-04-03 12:03:35 -0700315 try {
Patrick Baumann43c97a02018-01-31 20:09:03 +0000316 service.linkToDeath(InstantAppResolverConnection.this, 0 /*flags*/);
Todd Kennedy16c3a3e2017-04-03 12:03:35 -0700317 } catch (RemoteException e) {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700318 handleBinderDiedLocked();
Todd Kennedy16c3a3e2017-04-03 12:03:35 -0700319 }
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800320 mLock.notifyAll();
321 }
322 }
323
324 @Override
325 public void onServiceDisconnected(ComponentName name) {
Patrick Baumann43c97a02018-01-31 20:09:03 +0000326 if (DEBUG_INSTANT) {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700327 Slog.d(TAG, "Disconnected from instant app resolver");
Todd Kennedy7cf918e2017-04-10 15:10:56 -0700328 }
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800329 synchronized (mLock) {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700330 handleBinderDiedLocked();
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800331 }
332 }
333 }
334
Patrick Baumann43c97a02018-01-31 20:09:03 +0000335 private static final class GetInstantAppResolveInfoCaller
Todd Kennedy1fb34042017-03-01 13:56:58 -0800336 extends TimedRemoteCaller<List<InstantAppResolveInfo>> {
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800337 private final IRemoteCallback mCallback;
338
Patrick Baumann43c97a02018-01-31 20:09:03 +0000339 public GetInstantAppResolveInfoCaller() {
Todd Kennedy46b4f2b2017-04-21 12:20:03 -0700340 super(CALL_SERVICE_TIMEOUT_MS);
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800341 mCallback = new IRemoteCallback.Stub() {
342 @Override
343 public void sendResult(Bundle data) throws RemoteException {
Todd Kennedy1fb34042017-03-01 13:56:58 -0800344 final ArrayList<InstantAppResolveInfo> resolveList =
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800345 data.getParcelableArrayList(
Todd Kennedy1fb34042017-03-01 13:56:58 -0800346 InstantAppResolverService.EXTRA_RESOLVE_INFO);
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800347 int sequence =
Todd Kennedy1fb34042017-03-01 13:56:58 -0800348 data.getInt(InstantAppResolverService.EXTRA_SEQUENCE, -1);
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800349 onRemoteMethodResult(resolveList, sequence);
350 }
351 };
352 }
353
Patrick Baumann43c97a02018-01-31 20:09:03 +0000354 public List<InstantAppResolveInfo> getInstantAppResolveInfoList(
Patrick Baumann577d4022018-01-31 16:55:10 +0000355 IInstantAppResolver target, Intent sanitizedIntent, int hashPrefix[], String token)
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800356 throws RemoteException, TimeoutException {
357 final int sequence = onBeforeRemoteCall();
Patrick Baumann577d4022018-01-31 16:55:10 +0000358 target.getInstantAppResolveInfoList(sanitizedIntent, hashPrefix, token, sequence,
359 mCallback);
Todd Kennedye5195dd2016-10-19 15:29:19 -0700360 return getResultTimed(sequence);
361 }
362 }
Todd Kennedyb8a279e2015-11-18 09:59:47 -0800363}