1 /* 2 * Copyright (C) 2014 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 17 package android.media.tv; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.graphics.Rect; 22 import android.media.PlaybackParams; 23 import android.net.Uri; 24 import android.os.Bundle; 25 import android.os.IBinder; 26 import android.os.Looper; 27 import android.os.Message; 28 import android.util.Log; 29 import android.view.InputChannel; 30 import android.view.InputEvent; 31 import android.view.InputEventReceiver; 32 import android.view.Surface; 33 34 import com.android.internal.os.HandlerCaller; 35 import com.android.internal.os.SomeArgs; 36 37 /** 38 * Implements the internal ITvInputSession interface to convert incoming calls on to it back to 39 * calls on the public TvInputSession interface, scheduling them on the main thread of the process. 40 * 41 * @hide 42 */ 43 public class ITvInputSessionWrapper extends ITvInputSession.Stub implements HandlerCaller.Callback { 44 private static final String TAG = "TvInputSessionWrapper"; 45 46 private static final int EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS = 50; 47 private static final int EXECUTE_MESSAGE_TUNE_TIMEOUT_MILLIS = 2000; 48 private static final int EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS = 5 * 1000; 49 50 private static final int DO_RELEASE = 1; 51 private static final int DO_SET_MAIN = 2; 52 private static final int DO_SET_SURFACE = 3; 53 private static final int DO_DISPATCH_SURFACE_CHANGED = 4; 54 private static final int DO_SET_STREAM_VOLUME = 5; 55 private static final int DO_TUNE = 6; 56 private static final int DO_SET_CAPTION_ENABLED = 7; 57 private static final int DO_SELECT_TRACK = 8; 58 private static final int DO_APP_PRIVATE_COMMAND = 9; 59 private static final int DO_CREATE_OVERLAY_VIEW = 10; 60 private static final int DO_RELAYOUT_OVERLAY_VIEW = 11; 61 private static final int DO_REMOVE_OVERLAY_VIEW = 12; 62 private static final int DO_UNBLOCK_CONTENT = 13; 63 private static final int DO_TIME_SHIFT_PLAY = 14; 64 private static final int DO_TIME_SHIFT_PAUSE = 15; 65 private static final int DO_TIME_SHIFT_RESUME = 16; 66 private static final int DO_TIME_SHIFT_SEEK_TO = 17; 67 private static final int DO_TIME_SHIFT_SET_PLAYBACK_PARAMS = 18; 68 private static final int DO_TIME_SHIFT_ENABLE_POSITION_TRACKING = 19; 69 private static final int DO_START_RECORDING = 20; 70 private static final int DO_STOP_RECORDING = 21; 71 private static final int DO_PAUSE_RECORDING = 22; 72 private static final int DO_RESUME_RECORDING = 23; 73 private static final int DO_REQUEST_BROADCAST_INFO = 24; 74 private static final int DO_REMOVE_BROADCAST_INFO = 25; 75 private static final int DO_SET_IAPP_NOTIFICATION_ENABLED = 26; 76 private static final int DO_REQUEST_AD = 27; 77 private static final int DO_NOTIFY_AD_BUFFER = 28; 78 private static final int DO_SELECT_AUDIO_PRESENTATION = 29; 79 private static final int DO_TIME_SHIFT_SET_MODE = 30; 80 private static final int DO_SET_TV_MESSAGE_ENABLED = 31; 81 private static final int DO_NOTIFY_TV_MESSAGE = 32; 82 private static final int DO_STOP_PLAYBACK = 33; 83 private static final int DO_RESUME_PLAYBACK = 34; 84 private static final int DO_SET_VIDEO_FROZEN = 35; 85 private static final int DO_NOTIFY_AD_SESSION_DATA = 36; 86 87 private final boolean mIsRecordingSession; 88 private final HandlerCaller mCaller; 89 90 private TvInputService.Session mTvInputSessionImpl; 91 private TvInputService.RecordingSession mTvInputRecordingSessionImpl; 92 93 private InputChannel mChannel; 94 private TvInputEventReceiver mReceiver; 95 ITvInputSessionWrapper(Context context, TvInputService.Session sessionImpl, InputChannel channel)96 public ITvInputSessionWrapper(Context context, TvInputService.Session sessionImpl, 97 InputChannel channel) { 98 mIsRecordingSession = false; 99 mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); 100 mTvInputSessionImpl = sessionImpl; 101 mChannel = channel; 102 if (channel != null) { 103 mReceiver = new TvInputEventReceiver(channel, context.getMainLooper()); 104 } 105 } 106 107 // For the recording session ITvInputSessionWrapper(Context context, TvInputService.RecordingSession recordingSessionImpl)108 public ITvInputSessionWrapper(Context context, 109 TvInputService.RecordingSession recordingSessionImpl) { 110 mIsRecordingSession = true; 111 mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); 112 mTvInputRecordingSessionImpl = recordingSessionImpl; 113 } 114 115 @Override executeMessage(Message msg)116 public void executeMessage(Message msg) { 117 if ((mIsRecordingSession && mTvInputRecordingSessionImpl == null) 118 || (!mIsRecordingSession && mTvInputSessionImpl == null)) { 119 return; 120 } 121 122 long startTime = System.nanoTime(); 123 switch (msg.what) { 124 case DO_RELEASE: { 125 if (mIsRecordingSession) { 126 mTvInputRecordingSessionImpl.release(); 127 mTvInputRecordingSessionImpl = null; 128 } else { 129 mTvInputSessionImpl.release(); 130 mTvInputSessionImpl = null; 131 if (mReceiver != null) { 132 mReceiver.dispose(); 133 mReceiver = null; 134 } 135 if (mChannel != null) { 136 mChannel.dispose(); 137 mChannel = null; 138 } 139 } 140 break; 141 } 142 case DO_SET_MAIN: { 143 mTvInputSessionImpl.setMain((Boolean) msg.obj); 144 break; 145 } 146 case DO_SET_SURFACE: { 147 mTvInputSessionImpl.setSurface((Surface) msg.obj); 148 break; 149 } 150 case DO_DISPATCH_SURFACE_CHANGED: { 151 SomeArgs args = (SomeArgs) msg.obj; 152 mTvInputSessionImpl.dispatchSurfaceChanged(args.argi1, args.argi2, args.argi3); 153 args.recycle(); 154 break; 155 } 156 case DO_SET_STREAM_VOLUME: { 157 mTvInputSessionImpl.setStreamVolume((Float) msg.obj); 158 break; 159 } 160 case DO_TUNE: { 161 SomeArgs args = (SomeArgs) msg.obj; 162 if (mIsRecordingSession) { 163 mTvInputRecordingSessionImpl.tune((Uri) args.arg1, (Bundle) args.arg2); 164 } else { 165 mTvInputSessionImpl.tune((Uri) args.arg1, (Bundle) args.arg2); 166 } 167 args.recycle(); 168 break; 169 } 170 case DO_SET_CAPTION_ENABLED: { 171 mTvInputSessionImpl.setCaptionEnabled((Boolean) msg.obj); 172 break; 173 } 174 case DO_SELECT_TRACK: { 175 SomeArgs args = (SomeArgs) msg.obj; 176 mTvInputSessionImpl.selectTrack((Integer) args.arg1, (String) args.arg2); 177 args.recycle(); 178 break; 179 } 180 case DO_APP_PRIVATE_COMMAND: { 181 SomeArgs args = (SomeArgs) msg.obj; 182 if (mIsRecordingSession) { 183 mTvInputRecordingSessionImpl.appPrivateCommand( 184 (String) args.arg1, (Bundle) args.arg2); 185 } else { 186 mTvInputSessionImpl.appPrivateCommand((String) args.arg1, (Bundle) args.arg2); 187 } 188 args.recycle(); 189 break; 190 } 191 case DO_CREATE_OVERLAY_VIEW: { 192 SomeArgs args = (SomeArgs) msg.obj; 193 mTvInputSessionImpl.createOverlayView((IBinder) args.arg1, (Rect) args.arg2); 194 args.recycle(); 195 break; 196 } 197 case DO_RELAYOUT_OVERLAY_VIEW: { 198 mTvInputSessionImpl.relayoutOverlayView((Rect) msg.obj); 199 break; 200 } 201 case DO_REMOVE_OVERLAY_VIEW: { 202 mTvInputSessionImpl.removeOverlayView(true); 203 break; 204 } 205 case DO_UNBLOCK_CONTENT: { 206 mTvInputSessionImpl.unblockContent((String) msg.obj); 207 break; 208 } 209 case DO_TIME_SHIFT_PLAY: { 210 mTvInputSessionImpl.timeShiftPlay((Uri) msg.obj); 211 break; 212 } 213 case DO_TIME_SHIFT_PAUSE: { 214 mTvInputSessionImpl.timeShiftPause(); 215 break; 216 } 217 case DO_TIME_SHIFT_RESUME: { 218 mTvInputSessionImpl.timeShiftResume(); 219 break; 220 } 221 case DO_TIME_SHIFT_SEEK_TO: { 222 mTvInputSessionImpl.timeShiftSeekTo((Long) msg.obj); 223 break; 224 } 225 case DO_TIME_SHIFT_SET_PLAYBACK_PARAMS: { 226 mTvInputSessionImpl.timeShiftSetPlaybackParams((PlaybackParams) msg.obj); 227 break; 228 } 229 case DO_TIME_SHIFT_SET_MODE: { 230 mTvInputSessionImpl.timeShiftSetMode(msg.arg1); 231 break; 232 } 233 case DO_TIME_SHIFT_ENABLE_POSITION_TRACKING: { 234 mTvInputSessionImpl.timeShiftEnablePositionTracking((Boolean) msg.obj); 235 break; 236 } 237 case DO_START_RECORDING: { 238 SomeArgs args = (SomeArgs) msg.obj; 239 mTvInputRecordingSessionImpl.startRecording((Uri) args.arg1, (Bundle) args.arg2); 240 args.recycle(); 241 break; 242 } 243 case DO_STOP_RECORDING: { 244 mTvInputRecordingSessionImpl.stopRecording(); 245 break; 246 } 247 case DO_PAUSE_RECORDING: { 248 mTvInputRecordingSessionImpl.pauseRecording((Bundle) msg.obj); 249 break; 250 } 251 case DO_RESUME_RECORDING: { 252 mTvInputRecordingSessionImpl.resumeRecording((Bundle) msg.obj); 253 break; 254 } 255 case DO_SELECT_AUDIO_PRESENTATION: { 256 SomeArgs args = (SomeArgs) msg.obj; 257 mTvInputSessionImpl.selectAudioPresentation( 258 (Integer) args.arg1, (Integer) args.arg2); 259 args.recycle(); 260 break; 261 } 262 case DO_REQUEST_BROADCAST_INFO: { 263 mTvInputSessionImpl.requestBroadcastInfo((BroadcastInfoRequest) msg.obj); 264 break; 265 } 266 case DO_REMOVE_BROADCAST_INFO: { 267 mTvInputSessionImpl.removeBroadcastInfo(msg.arg1); 268 break; 269 } 270 case DO_SET_IAPP_NOTIFICATION_ENABLED: { 271 mTvInputSessionImpl.setInteractiveAppNotificationEnabled((Boolean) msg.obj); 272 break; 273 } 274 case DO_SET_TV_MESSAGE_ENABLED: { 275 SomeArgs args = (SomeArgs) msg.obj; 276 mTvInputSessionImpl.setTvMessageEnabled((Integer) args.arg1, (Boolean) args.arg2); 277 args.recycle(); 278 break; 279 } 280 case DO_REQUEST_AD: { 281 mTvInputSessionImpl.requestAd((AdRequest) msg.obj); 282 break; 283 } 284 case DO_NOTIFY_AD_BUFFER: { 285 mTvInputSessionImpl.notifyAdBufferReady((AdBuffer) msg.obj); 286 break; 287 } 288 case DO_NOTIFY_TV_MESSAGE: { 289 SomeArgs args = (SomeArgs) msg.obj; 290 mTvInputSessionImpl.onTvMessageReceived((Integer) args.arg1, (Bundle) args.arg2); 291 args.recycle(); 292 break; 293 } 294 case DO_STOP_PLAYBACK: { 295 mTvInputSessionImpl.stopPlayback(msg.arg1); 296 break; 297 } 298 case DO_RESUME_PLAYBACK: { 299 mTvInputSessionImpl.resumePlayback(); 300 break; 301 } 302 case DO_SET_VIDEO_FROZEN: { 303 mTvInputSessionImpl.setVideoFrozen((Boolean) msg.obj); 304 break; 305 } 306 case DO_NOTIFY_AD_SESSION_DATA: { 307 SomeArgs args = (SomeArgs) msg.obj; 308 mTvInputSessionImpl.notifyTvAdSessionData((String) args.arg1, (Bundle) args.arg2); 309 args.recycle(); 310 break; 311 } 312 default: { 313 Log.w(TAG, "Unhandled message code: " + msg.what); 314 break; 315 } 316 } 317 long durationMs = (System.nanoTime() - startTime) / (1000 * 1000); 318 if (durationMs > EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS) { 319 Log.w(TAG, "Handling message (" + msg.what + ") took too long time (duration=" 320 + durationMs + "ms)"); 321 if (msg.what == DO_TUNE && durationMs > EXECUTE_MESSAGE_TUNE_TIMEOUT_MILLIS) { 322 throw new RuntimeException("Too much time to handle tune request. (" + durationMs 323 + "ms > " + EXECUTE_MESSAGE_TUNE_TIMEOUT_MILLIS + "ms) " 324 + "Consider handling the tune request in a separate thread."); 325 } 326 if (durationMs > EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS) { 327 throw new RuntimeException("Too much time to handle a request. (type=" + msg.what 328 + ", " + durationMs + "ms > " + EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS + "ms)."); 329 } 330 } 331 } 332 333 @Override release()334 public void release() { 335 if (!mIsRecordingSession) { 336 mTvInputSessionImpl.scheduleOverlayViewCleanup(); 337 } 338 mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); 339 } 340 341 @Override setMain(boolean isMain)342 public void setMain(boolean isMain) { 343 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_MAIN, isMain)); 344 } 345 346 @Override setSurface(Surface surface)347 public void setSurface(Surface surface) { 348 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface)); 349 } 350 351 @Override dispatchSurfaceChanged(int format, int width, int height)352 public void dispatchSurfaceChanged(int format, int width, int height) { 353 mCaller.executeOrSendMessage(mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, 354 format, width, height, 0)); 355 } 356 357 @Override setVolume(float volume)358 public final void setVolume(float volume) { 359 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_STREAM_VOLUME, volume)); 360 } 361 362 @Override tune(Uri channelUri, Bundle params)363 public void tune(Uri channelUri, Bundle params) { 364 // Clear the pending tune requests. 365 mCaller.removeMessages(DO_TUNE); 366 mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_TUNE, channelUri, params)); 367 } 368 369 @Override setCaptionEnabled(boolean enabled)370 public void setCaptionEnabled(boolean enabled) { 371 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_CAPTION_ENABLED, enabled)); 372 } 373 374 @Override selectAudioPresentation(int presentationId, int programId)375 public void selectAudioPresentation(int presentationId, int programId) { 376 mCaller.executeOrSendMessage( 377 mCaller.obtainMessageOO(DO_SELECT_AUDIO_PRESENTATION, presentationId, programId)); 378 } 379 380 @Override selectTrack(int type, String trackId)381 public void selectTrack(int type, String trackId) { 382 mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_SELECT_TRACK, type, trackId)); 383 } 384 385 @Override setInteractiveAppNotificationEnabled(boolean enabled)386 public void setInteractiveAppNotificationEnabled(boolean enabled) { 387 mCaller.executeOrSendMessage( 388 mCaller.obtainMessageO(DO_SET_IAPP_NOTIFICATION_ENABLED, enabled)); 389 } 390 391 @Override appPrivateCommand(String action, Bundle data)392 public void appPrivateCommand(String action, Bundle data) { 393 mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_APP_PRIVATE_COMMAND, action, 394 data)); 395 } 396 397 @Override createOverlayView(IBinder windowToken, Rect frame)398 public void createOverlayView(IBinder windowToken, Rect frame) { 399 mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_OVERLAY_VIEW, windowToken, 400 frame)); 401 } 402 403 @Override relayoutOverlayView(Rect frame)404 public void relayoutOverlayView(Rect frame) { 405 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RELAYOUT_OVERLAY_VIEW, frame)); 406 } 407 408 @Override removeOverlayView()409 public void removeOverlayView() { 410 mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_OVERLAY_VIEW)); 411 } 412 413 @Override unblockContent(String unblockedRating)414 public void unblockContent(String unblockedRating) { 415 mCaller.executeOrSendMessage(mCaller.obtainMessageO( 416 DO_UNBLOCK_CONTENT, unblockedRating)); 417 } 418 419 @Override timeShiftPlay(Uri recordedProgramUri)420 public void timeShiftPlay(Uri recordedProgramUri) { 421 mCaller.executeOrSendMessage(mCaller.obtainMessageO( 422 DO_TIME_SHIFT_PLAY, recordedProgramUri)); 423 } 424 425 @Override timeShiftPause()426 public void timeShiftPause() { 427 mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_TIME_SHIFT_PAUSE)); 428 } 429 430 @Override timeShiftResume()431 public void timeShiftResume() { 432 mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_TIME_SHIFT_RESUME)); 433 } 434 435 @Override timeShiftSeekTo(long timeMs)436 public void timeShiftSeekTo(long timeMs) { 437 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TIME_SHIFT_SEEK_TO, timeMs)); 438 } 439 440 @Override timeShiftSetPlaybackParams(PlaybackParams params)441 public void timeShiftSetPlaybackParams(PlaybackParams params) { 442 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TIME_SHIFT_SET_PLAYBACK_PARAMS, 443 params)); 444 } 445 446 @Override timeShiftSetMode(int mode)447 public void timeShiftSetMode(int mode) { 448 mCaller.executeOrSendMessage(mCaller.obtainMessageI(DO_TIME_SHIFT_SET_MODE, mode)); 449 } 450 451 @Override timeShiftEnablePositionTracking(boolean enable)452 public void timeShiftEnablePositionTracking(boolean enable) { 453 mCaller.executeOrSendMessage(mCaller.obtainMessageO( 454 DO_TIME_SHIFT_ENABLE_POSITION_TRACKING, enable)); 455 } 456 457 @Override startRecording(@ullable Uri programUri, @Nullable Bundle params)458 public void startRecording(@Nullable Uri programUri, @Nullable Bundle params) { 459 mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_START_RECORDING, programUri, 460 params)); 461 } 462 463 @Override stopRecording()464 public void stopRecording() { 465 mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_STOP_RECORDING)); 466 } 467 468 @Override pauseRecording(@ullable Bundle params)469 public void pauseRecording(@Nullable Bundle params) { 470 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_PAUSE_RECORDING, params)); 471 } 472 473 @Override resumeRecording(@ullable Bundle params)474 public void resumeRecording(@Nullable Bundle params) { 475 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RESUME_RECORDING, params)); 476 } 477 478 @Override requestBroadcastInfo(BroadcastInfoRequest request)479 public void requestBroadcastInfo(BroadcastInfoRequest request) { 480 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_REQUEST_BROADCAST_INFO, request)); 481 } 482 483 @Override removeBroadcastInfo(int requestId)484 public void removeBroadcastInfo(int requestId) { 485 mCaller.executeOrSendMessage(mCaller.obtainMessageI(DO_REMOVE_BROADCAST_INFO, requestId)); 486 } 487 488 @Override requestAd(AdRequest request)489 public void requestAd(AdRequest request) { 490 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_REQUEST_AD, request)); 491 } 492 493 @Override notifyAdBufferReady(AdBuffer buffer)494 public void notifyAdBufferReady(AdBuffer buffer) { 495 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_AD_BUFFER, buffer)); 496 } 497 498 @Override notifyTvAdSessionData(String type, Bundle data)499 public void notifyTvAdSessionData(String type, Bundle data) { 500 mCaller.executeOrSendMessage( 501 mCaller.obtainMessageOO(DO_NOTIFY_AD_SESSION_DATA, type, data)); 502 } 503 504 @Override setVideoFrozen(boolean isFrozen)505 public void setVideoFrozen(boolean isFrozen) { 506 mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_VIDEO_FROZEN, isFrozen)); 507 } 508 509 @Override notifyTvMessage(int type, Bundle data)510 public void notifyTvMessage(int type, Bundle data) { 511 mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_NOTIFY_TV_MESSAGE, type, data)); 512 } 513 514 @Override setTvMessageEnabled(int type, boolean enabled)515 public void setTvMessageEnabled(int type, boolean enabled) { 516 mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_SET_TV_MESSAGE_ENABLED, type, 517 enabled)); 518 } 519 520 @Override stopPlayback(int mode)521 public void stopPlayback(int mode) { 522 mCaller.executeOrSendMessage(mCaller.obtainMessageI(DO_STOP_PLAYBACK, mode)); 523 } 524 525 @Override resumePlayback()526 public void resumePlayback() { 527 mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RESUME_PLAYBACK)); 528 } 529 530 531 private final class TvInputEventReceiver extends InputEventReceiver { TvInputEventReceiver(InputChannel inputChannel, Looper looper)532 TvInputEventReceiver(InputChannel inputChannel, Looper looper) { 533 super(inputChannel, looper); 534 } 535 536 @Override onInputEvent(InputEvent event)537 public void onInputEvent(InputEvent event) { 538 if (mTvInputSessionImpl == null) { 539 // The session has been finished. 540 finishInputEvent(event, false); 541 return; 542 } 543 544 int handled = mTvInputSessionImpl.dispatchInputEvent(event, this); 545 if (handled != TvInputManager.Session.DISPATCH_IN_PROGRESS) { 546 finishInputEvent(event, handled == TvInputManager.Session.DISPATCH_HANDLED); 547 } 548 } 549 } 550 } 551