VideoProviderTest.java revision 115c06ee64e209cda99abdc1fbd23fd65aa6da47
1/*
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.telecom.tests;
18
19import org.mockito.ArgumentCaptor;
20import org.mockito.Mock;
21import org.mockito.Mockito;
22import org.mockito.internal.exceptions.ExceptionIncludingMockitoWarnings;
23import org.mockito.invocation.InvocationOnMock;
24import org.mockito.stubbing.Answer;
25
26import android.app.AppOpsManager;
27import android.content.Context;
28import android.graphics.SurfaceTexture;
29import android.net.Uri;
30import android.os.Build;
31import android.os.Handler;
32import android.os.Looper;
33import android.os.UserHandle;
34import android.telecom.Connection.VideoProvider;
35import android.telecom.InCallService;
36import android.telecom.InCallService.VideoCall;
37import android.telecom.VideoCallImpl;
38import android.telecom.VideoProfile;
39import android.telecom.VideoProfile.CameraCapabilities;
40import android.test.suitebuilder.annotation.MediumTest;
41import android.view.Surface;
42
43import com.google.common.base.Predicate;
44
45import java.util.List;
46import java.util.concurrent.CountDownLatch;
47import java.util.concurrent.TimeUnit;
48
49import static android.test.MoreAsserts.assertEquals;
50import static org.mockito.Matchers.any;
51import static org.mockito.Matchers.anyInt;
52import static org.mockito.Matchers.anyLong;
53import static org.mockito.Matchers.anyString;
54import static org.mockito.Matchers.eq;
55import static org.mockito.Mockito.doAnswer;
56import static org.mockito.Mockito.doNothing;
57import static org.mockito.Mockito.doReturn;
58import static org.mockito.Mockito.doThrow;
59import static org.mockito.Mockito.times;
60import static org.mockito.Mockito.timeout;
61import static org.mockito.Mockito.mock;
62import static org.mockito.Mockito.verify;
63import static org.mockito.Mockito.when;
64
65/**
66 * Performs tests of the {@link VideoProvider} and {@link VideoCall} APIs.  Ensures that requests
67 * sent from an InCallService are routed through Telecom to a VideoProvider, and that callbacks are
68 * correctly routed.
69 */
70public class VideoProviderTest extends TelecomSystemTest {
71    private static final int ORIENTATION_0 = 0;
72    private static final int ORIENTATION_90 = 90;
73    private static final float ZOOM_LEVEL = 3.0f;
74
75    @Mock private VideoCall.Callback mVideoCallCallback;
76    private IdPair mCallIds;
77    private InCallService.VideoCall mVideoCall;
78    private VideoCallImpl mVideoCallImpl;
79    private ConnectionServiceFixture.ConnectionInfo mConnectionInfo;
80    private CountDownLatch mVerificationLock;
81    private AppOpsManager mAppOpsManager;
82
83    private Answer mVerification = new Answer() {
84        @Override
85        public Object answer(InvocationOnMock i) {
86            mVerificationLock.countDown();
87            return null;
88        }
89    };
90
91    @Override
92    public void setUp() throws Exception {
93        super.setUp();
94        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
95        mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
96
97        mCallIds = startAndMakeActiveOutgoingCall(
98                "650-555-1212",
99                mPhoneAccountA0.getAccountHandle(),
100                mConnectionServiceFixtureA);
101
102        // Set the video provider on the connection.
103        mConnectionServiceFixtureA.sendSetVideoProvider(
104                mConnectionServiceFixtureA.mLatestConnectionId);
105
106        // Provide a mocked VideoCall.Callback to receive callbacks via.
107        mVideoCallCallback = mock(InCallService.VideoCall.Callback.class);
108
109        mVideoCall = mInCallServiceFixtureX.getCall(mCallIds.mCallId).getVideoCallImpl(
110                mInCallServiceComponentNameX.getPackageName(), Build.VERSION.SDK_INT);
111        mVideoCallImpl = (VideoCallImpl) mVideoCall;
112        mVideoCall.registerCallback(mVideoCallCallback);
113
114        mConnectionInfo = mConnectionServiceFixtureA.mConnectionById.get(mCallIds.mConnectionId);
115        mVerificationLock = new CountDownLatch(1);
116        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
117
118        doNothing().when(mContext).enforcePermission(anyString(), anyInt(), anyInt(), anyString());
119        doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).noteOp(anyInt(), anyInt(),
120                anyString());
121    }
122
123    @Override
124    public void tearDown() throws Exception {
125        super.tearDown();
126    }
127
128    /**
129     * Tests the {@link VideoCall#setCamera(String)}, {@link VideoProvider#onSetCamera(String)},
130     * and {@link VideoCall.Callback#onCameraCapabilitiesChanged(CameraCapabilities)}
131     * APIS.
132     */
133    @MediumTest
134    public void testCameraChange() throws Exception {
135        // Wait until the callback has been received before performing verification.
136        doAnswer(mVerification).when(mVideoCallCallback)
137                .onCameraCapabilitiesChanged(any(CameraCapabilities.class));
138
139        // Make 2 setCamera requests.
140        mVideoCall.setCamera(MockVideoProvider.CAMERA_FRONT);
141        mVideoCall.setCamera(MockVideoProvider.CAMERA_BACK);
142
143        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
144
145        // Capture the video profile reported via the callback.
146        ArgumentCaptor<CameraCapabilities> cameraCapabilitiesCaptor =
147                ArgumentCaptor.forClass(CameraCapabilities.class);
148
149        // Verify that the callback was called twice and capture the callback arguments.
150        verify(mVideoCallCallback, timeout(TEST_TIMEOUT).times(2))
151                .onCameraCapabilitiesChanged(cameraCapabilitiesCaptor.capture());
152
153        assertEquals(2, cameraCapabilitiesCaptor.getAllValues().size());
154
155        List<CameraCapabilities> cameraCapabilities = cameraCapabilitiesCaptor.getAllValues();
156        // Ensure dimensions are as expected.
157        assertEquals(MockVideoProvider.CAMERA_FRONT_DIMENSIONS,
158                cameraCapabilities.get(0).getHeight());
159        assertEquals(MockVideoProvider.CAMERA_BACK_DIMENSIONS,
160                cameraCapabilities.get(1).getHeight());
161    }
162
163    /**
164     * Tests the caller permission check in {@link VideoCall#setCamera(String)} to ensure a camera
165     * change from a non-permitted caller is ignored.
166     */
167    @MediumTest
168    public void testCameraChangePermissionFail() throws Exception {
169        // Wait until the callback has been received before performing verification.
170        doAnswer(mVerification).when(mVideoCallCallback).onCallSessionEvent(anyInt());
171
172        // ensure permission check fails.
173        doThrow(new SecurityException()).when(mContext)
174                .enforcePermission(anyString(), anyInt(), anyInt(), anyString());
175
176        // Make a request to change the camera
177        mVideoCall.setCamera(MockVideoProvider.CAMERA_FRONT);
178        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
179
180        // Capture the session event reported via the callback.
181        ArgumentCaptor<Integer> sessionEventCaptor = ArgumentCaptor.forClass(Integer.class);
182        verify(mVideoCallCallback, timeout(TEST_TIMEOUT)).onCallSessionEvent(
183                sessionEventCaptor.capture());
184
185        assertEquals(VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR,
186                sessionEventCaptor.getValue().intValue());
187    }
188
189    /**
190     * Tests the caller app ops check in {@link VideoCall#setCamera(String)} to ensure a camera
191     * change from a non-permitted caller is ignored.
192     */
193    @MediumTest
194    public void testCameraChangeAppOpsFail() throws Exception {
195        // Wait until the callback has been received before performing verification.
196        doAnswer(mVerification).when(mVideoCallCallback).onCallSessionEvent(anyInt());
197
198        // ensure app ops check fails.
199        doReturn(AppOpsManager.MODE_ERRORED).when(mAppOpsManager).noteOp(anyInt(), anyInt(),
200                anyString());
201
202        // Make a request to change the camera
203        mVideoCall.setCamera(MockVideoProvider.CAMERA_FRONT);
204        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
205
206        // Capture the session event reported via the callback.
207        ArgumentCaptor<Integer> sessionEventCaptor = ArgumentCaptor.forClass(Integer.class);
208        verify(mVideoCallCallback, timeout(TEST_TIMEOUT)).onCallSessionEvent(
209                sessionEventCaptor.capture());
210
211        assertEquals(VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR,
212                sessionEventCaptor.getValue().intValue());
213    }
214
215    /**
216     * Tests the caller user handle check in {@link VideoCall#setCamera(String)} to ensure a camera
217     * change from a background user is not permitted.
218     */
219    @MediumTest
220    public void testCameraChangeUserFail() throws Exception {
221        // Wait until the callback has been received before performing verification.
222        doAnswer(mVerification).when(mVideoCallCallback).onCallSessionEvent(anyInt());
223
224        // Set a fake user to be the current foreground user.
225        mTelecomSystem.getCallsManager().onUserSwitch(new UserHandle(1000));
226
227        // Make a request to change the camera
228        mVideoCall.setCamera(MockVideoProvider.CAMERA_FRONT);
229        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
230
231        // Capture the session event reported via the callback.
232        ArgumentCaptor<Integer> sessionEventCaptor = ArgumentCaptor.forClass(Integer.class);
233        verify(mVideoCallCallback, timeout(TEST_TIMEOUT)).onCallSessionEvent(
234                sessionEventCaptor.capture());
235
236        assertEquals(VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR,
237                sessionEventCaptor.getValue().intValue());
238    }
239
240    /**
241     * Tests the caller permission check in {@link VideoCall#setCamera(String)} to ensure the
242     * caller can null out the camera, even if they do not have camera permission.
243     */
244    @MediumTest
245    public void testCameraChangeNullNoPermission() throws Exception {
246        // Wait until the callback has been received before performing verification.
247        doAnswer(mVerification).when(mVideoCallCallback).onCallSessionEvent(anyInt());
248
249        // ensure permission check fails.
250        doThrow(new SecurityException()).when(mContext)
251                .enforcePermission(anyString(), anyInt(), anyInt(), anyString());
252
253        // Make a request to null the camera; we expect the permission check won't happen.
254        mVideoCall.setCamera(null);
255        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
256
257        // Capture the session event reported via the callback.
258        ArgumentCaptor<Integer> sessionEventCaptor = ArgumentCaptor.forClass(Integer.class);
259        verify(mVideoCallCallback, timeout(TEST_TIMEOUT)).onCallSessionEvent(
260                sessionEventCaptor.capture());
261
262        // See the MockVideoProvider class; for convenience when the camera is nulled we just send
263        // back a "camera ready" event.
264        assertEquals(VideoProvider.SESSION_EVENT_CAMERA_READY,
265                sessionEventCaptor.getValue().intValue());
266    }
267
268    /**
269     * Tests the {@link VideoCall#setPreviewSurface(Surface)} and
270     * {@link VideoProvider#onSetPreviewSurface(Surface)} APIs.
271     */
272    @MediumTest
273    public void testSetPreviewSurface() throws Exception {
274        final Surface surface = new Surface(new SurfaceTexture(1));
275        mVideoCall.setPreviewSurface(surface);
276
277        assertTrueWithTimeout(new Predicate<Void>() {
278            @Override
279            public boolean apply(Void v) {
280                return mConnectionInfo.mockVideoProvider.getPreviewSurface() == surface;
281            }
282        });
283
284        mVideoCall.setPreviewSurface(null);
285
286        assertTrueWithTimeout(new Predicate<Void>() {
287            @Override
288            public boolean apply(Void v) {
289                return mConnectionInfo.mockVideoProvider.getPreviewSurface() == null;
290            }
291        });
292    }
293
294    /**
295     * Tests the {@link VideoCall#setDisplaySurface(Surface)} and
296     * {@link VideoProvider#onSetDisplaySurface(Surface)} APIs.
297     */
298    @MediumTest
299    public void testSetDisplaySurface() throws Exception {
300        final Surface surface = new Surface(new SurfaceTexture(1));
301        mVideoCall.setDisplaySurface(surface);
302
303        assertTrueWithTimeout(new Predicate<Void>() {
304            @Override
305            public boolean apply(Void v) {
306                return mConnectionInfo.mockVideoProvider.getDisplaySurface() == surface;
307            }
308        });
309
310        mVideoCall.setDisplaySurface(null);
311
312        assertTrueWithTimeout(new Predicate<Void>() {
313            @Override
314            public boolean apply(Void v) {
315                return mConnectionInfo.mockVideoProvider.getDisplaySurface() == null;
316            }
317        });
318    }
319
320    /**
321     * Tests the {@link VideoCall#setDeviceOrientation(int)} and
322     * {@link VideoProvider#onSetDeviceOrientation(int)} APIs.
323     */
324    @MediumTest
325    public void testSetDeviceOrientation() throws Exception {
326        mVideoCall.setDeviceOrientation(ORIENTATION_0);
327
328        assertTrueWithTimeout(new Predicate<Void>() {
329            @Override
330            public boolean apply(Void v) {
331                return mConnectionInfo.mockVideoProvider.getDeviceOrientation() == ORIENTATION_0;
332            }
333        });
334
335        mVideoCall.setDeviceOrientation(ORIENTATION_90);
336
337        assertTrueWithTimeout(new Predicate<Void>() {
338            @Override
339            public boolean apply(Void v) {
340                return mConnectionInfo.mockVideoProvider.getDeviceOrientation() == ORIENTATION_90;
341            }
342        });
343    }
344
345    /**
346     * Tests the {@link VideoCall#setZoom(float)} and {@link VideoProvider#onSetZoom(float)} APIs.
347     */
348    @MediumTest
349    public void testSetZoom() throws Exception {
350        mVideoCall.setZoom(ZOOM_LEVEL);
351
352        assertTrueWithTimeout(new Predicate<Void>() {
353            @Override
354            public boolean apply(Void v) {
355                return mConnectionInfo.mockVideoProvider.getZoom() == ZOOM_LEVEL;
356            }
357        });
358    }
359
360    /**
361     * Tests the {@link VideoCall#sendSessionModifyRequest(VideoProfile)},
362     * {@link VideoProvider#onSendSessionModifyRequest(VideoProfile, VideoProfile)},
363     * {@link VideoProvider#receiveSessionModifyResponse(int, VideoProfile, VideoProfile)}, and
364     * {@link VideoCall.Callback#onSessionModifyResponseReceived(int, VideoProfile, VideoProfile)}
365     * APIs.
366     *
367     * Emulates a scenario where an InCallService sends a request to upgrade to video, which the
368     * peer accepts as-is.
369     */
370    @MediumTest
371    public void testSessionModifyRequest() throws Exception {
372        VideoProfile requestProfile = new VideoProfile(VideoProfile.STATE_BIDIRECTIONAL);
373
374        // Set the starting video state on the video call impl; normally this would be set based on
375        // the original android.telecom.Call instance.
376        mVideoCallImpl.setVideoState(VideoProfile.STATE_RX_ENABLED);
377
378        doAnswer(mVerification).when(mVideoCallCallback)
379                .onSessionModifyResponseReceived(anyInt(), any(VideoProfile.class),
380                        any(VideoProfile.class));
381
382        // Send the request.
383        mVideoCall.sendSessionModifyRequest(requestProfile);
384
385        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
386
387        // Capture the video profiles from the callback.
388        ArgumentCaptor<VideoProfile> fromVideoProfileCaptor =
389                ArgumentCaptor.forClass(VideoProfile.class);
390        ArgumentCaptor<VideoProfile> toVideoProfileCaptor =
391                ArgumentCaptor.forClass(VideoProfile.class);
392
393        // Verify we got a response and capture the profiles.
394        verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
395                .onSessionModifyResponseReceived(eq(VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS),
396                        fromVideoProfileCaptor.capture(), toVideoProfileCaptor.capture());
397
398        assertEquals(VideoProfile.STATE_RX_ENABLED,
399                fromVideoProfileCaptor.getValue().getVideoState());
400        assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
401                toVideoProfileCaptor.getValue().getVideoState());
402    }
403
404    /**
405     * Tests the {@link VideoCall#sendSessionModifyResponse(VideoProfile)},
406     * and {@link VideoProvider#onSendSessionModifyResponse(VideoProfile)} APIs.
407     */
408    @MediumTest
409    public void testSessionModifyResponse() throws Exception {
410        VideoProfile sessionModifyResponse = new VideoProfile(VideoProfile.STATE_TX_ENABLED);
411
412        mVideoCall.sendSessionModifyResponse(sessionModifyResponse);
413
414        assertTrueWithTimeout(new Predicate<Void>() {
415            @Override
416            public boolean apply(Void v) {
417                VideoProfile response = mConnectionInfo.mockVideoProvider
418                        .getSessionModifyResponse();
419                return response != null && response.getVideoState() == VideoProfile.STATE_TX_ENABLED;
420            }
421        });
422    }
423
424    /**
425     * Tests the {@link VideoCall#requestCameraCapabilities()} ()},
426     * {@link VideoProvider#onRequestCameraCapabilities()} ()}, and
427     * {@link VideoCall.Callback#onCameraCapabilitiesChanged(CameraCapabilities)} APIs.
428     */
429    @MediumTest
430    public void testRequestCameraCapabilities() throws Exception {
431        // Wait until the callback has been received before performing verification.
432        doAnswer(mVerification).when(mVideoCallCallback)
433                .onCameraCapabilitiesChanged(any(CameraCapabilities.class));
434
435        mVideoCall.requestCameraCapabilities();
436
437        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
438
439        verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
440                .onCameraCapabilitiesChanged(any(CameraCapabilities.class));
441    }
442
443    /**
444     * Tests the {@link VideoCall#setPauseImage(Uri)}, and
445     * {@link VideoProvider#onSetPauseImage(Uri)} APIs.
446     */
447    @MediumTest
448    public void testSetPauseImage() throws Exception {
449        final Uri testUri = Uri.fromParts("file", "test.jpg", null);
450        mVideoCall.setPauseImage(testUri);
451
452        assertTrueWithTimeout(new Predicate<Void>() {
453            @Override
454            public boolean apply(Void v) {
455                Uri pauseImage = mConnectionInfo.mockVideoProvider.getPauseImage();
456                return pauseImage != null && pauseImage.equals(testUri);
457            }
458        });
459    }
460
461    /**
462     * Tests the {@link VideoCall#requestCallDataUsage()},
463     * {@link VideoProvider#onRequestConnectionDataUsage()}, and
464     * {@link VideoCall.Callback#onCallDataUsageChanged(long)} APIs.
465     */
466    @MediumTest
467    public void testRequestDataUsage() throws Exception {
468        // Wait until the callback has been received before performing verification.
469        doAnswer(mVerification).when(mVideoCallCallback)
470                .onCallDataUsageChanged(anyLong());
471
472        mVideoCall.requestCallDataUsage();
473
474        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
475
476        verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
477                .onCallDataUsageChanged(eq(MockVideoProvider.DATA_USAGE));
478    }
479
480    /**
481     * Tests the {@link VideoProvider#receiveSessionModifyRequest(VideoProfile)},
482     * {@link VideoCall.Callback#onSessionModifyRequestReceived(VideoProfile)} APIs.
483     */
484    @MediumTest
485    public void testReceiveSessionModifyRequest() throws Exception {
486        // Wait until the callback has been received before performing verification.
487        doAnswer(mVerification).when(mVideoCallCallback)
488                .onSessionModifyRequestReceived(any(VideoProfile.class));
489
490        mConnectionInfo.mockVideoProvider.sendMockSessionModifyRequest();
491
492        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
493
494        ArgumentCaptor<VideoProfile> requestProfileCaptor =
495                ArgumentCaptor.forClass(VideoProfile.class);
496        verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
497                .onSessionModifyRequestReceived(requestProfileCaptor.capture());
498        assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
499                requestProfileCaptor.getValue().getVideoState());
500    }
501
502
503    /**
504     * Tests the {@link VideoProvider#handleCallSessionEvent(int)}, and
505     * {@link VideoCall.Callback#onCallSessionEvent(int)} APIs.
506     */
507    @MediumTest
508    public void testSessionEvent() throws Exception {
509        // Wait until the callback has been received before performing verification.
510        doAnswer(mVerification).when(mVideoCallCallback)
511                .onCallSessionEvent(anyInt());
512
513        mConnectionInfo.mockVideoProvider.sendMockSessionEvent(
514                VideoProvider.SESSION_EVENT_CAMERA_READY);
515
516        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
517
518        verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
519                .onCallSessionEvent(eq(VideoProvider.SESSION_EVENT_CAMERA_READY));
520    }
521
522    /**
523     * Tests the {@link VideoProvider#changePeerDimensions(int, int)} and
524     * {@link VideoCall.Callback#onPeerDimensionsChanged(int, int)} APIs.
525     */
526    @MediumTest
527    public void testPeerDimensionChange() throws Exception {
528        // Wait until the callback has been received before performing verification.
529        doAnswer(mVerification).when(mVideoCallCallback)
530                .onPeerDimensionsChanged(anyInt(), anyInt());
531
532        mConnectionInfo.mockVideoProvider.sendMockPeerDimensions(MockVideoProvider.PEER_DIMENSIONS,
533                MockVideoProvider.PEER_DIMENSIONS);
534
535        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
536
537        verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
538                .onPeerDimensionsChanged(eq(MockVideoProvider.PEER_DIMENSIONS),
539                        eq(MockVideoProvider.PEER_DIMENSIONS));
540    }
541
542    /**
543     * Tests the {@link VideoProvider#changeVideoQuality(int)} and
544     * {@link VideoCall.Callback#onVideoQualityChanged(int)} APIs.
545     */
546    @MediumTest
547    public void testVideoQualityChange() throws Exception {
548        // Wait until the callback has been received before performing verification.
549        doAnswer(mVerification).when(mVideoCallCallback)
550                .onVideoQualityChanged(anyInt());
551
552        mConnectionInfo.mockVideoProvider.sendMockVideoQuality(VideoProfile.QUALITY_HIGH);
553
554        mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
555
556        verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
557                .onVideoQualityChanged(eq(VideoProfile.QUALITY_HIGH));
558    }
559}
560