1 /* 2 * Copyright (C) 2016 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; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.ActivityThread; 22 import android.app.AppOpsManager; 23 import android.content.Context; 24 import android.os.IBinder; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.os.Process; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import com.android.internal.annotations.GuardedBy; 34 import com.android.internal.app.IAppOpsCallback; 35 import com.android.internal.app.IAppOpsService; 36 37 import java.lang.ref.WeakReference; 38 import java.util.Objects; 39 40 /** 41 * Class to encapsulate a number of common player operations: 42 * - AppOps for OP_PLAY_AUDIO 43 * - more to come (routing, transport control) 44 * @hide 45 */ 46 public abstract class PlayerBase { 47 48 private static final String TAG = "PlayerBase"; 49 /** Debug app ops */ 50 private static final boolean DEBUG_APP_OPS = false; 51 private static final boolean DEBUG = DEBUG_APP_OPS || false; 52 private static IAudioService sService; //lazy initialization, use getService() 53 54 /** if true, only use OP_PLAY_AUDIO monitoring for logging, and rely on muting to happen 55 * in AudioFlinger */ 56 private static final boolean USE_AUDIOFLINGER_MUTING_FOR_OP = true; 57 58 // parameters of the player that affect AppOps 59 protected AudioAttributes mAttributes; 60 61 // volumes of the subclass "player volumes", as seen by the client of the subclass 62 // (e.g. what was passed in AudioTrack.setVolume(float)). The actual volume applied is 63 // the combination of the player volume, and the PlayerBase pan and volume multipliers 64 protected float mLeftVolume = 1.0f; 65 protected float mRightVolume = 1.0f; 66 protected float mAuxEffectSendLevel = 0.0f; 67 68 // NEVER call into AudioService (see getService()) with mLock held: PlayerBase can run in 69 // the same process as AudioService, which can synchronously call back into this class, 70 // causing deadlocks between the two 71 private final Object mLock = new Object(); 72 73 // for AppOps 74 private @Nullable IAppOpsService mAppOps; 75 private @Nullable IAppOpsCallback mAppOpsCallback; 76 @GuardedBy("mLock") 77 private boolean mHasAppOpsPlayAudio = true; 78 79 private final int mImplType; 80 // uniquely identifies the Player Interface throughout the system (P I Id) 81 private int mPlayerIId = AudioPlaybackConfiguration.PLAYER_PIID_INVALID; 82 83 @GuardedBy("mLock") 84 private int mState; 85 @GuardedBy("mLock") 86 private int mStartDelayMs = 0; 87 @GuardedBy("mLock") 88 private float mPanMultiplierL = 1.0f; 89 @GuardedBy("mLock") 90 private float mPanMultiplierR = 1.0f; 91 @GuardedBy("mLock") 92 private float mVolMultiplier = 1.0f; 93 94 /** 95 * Constructor. Must be given audio attributes, as they are required for AppOps. 96 * @param attr non-null audio attributes 97 * @param class non-null class of the implementation of this abstract class 98 */ PlayerBase(@onNull AudioAttributes attr, int implType)99 PlayerBase(@NonNull AudioAttributes attr, int implType) { 100 if (attr == null) { 101 throw new IllegalArgumentException("Illegal null AudioAttributes"); 102 } 103 mAttributes = attr; 104 mImplType = implType; 105 mState = AudioPlaybackConfiguration.PLAYER_STATE_IDLE; 106 }; 107 108 /** 109 * Call from derived class when instantiation / initialization is successful 110 */ baseRegisterPlayer()111 protected void baseRegisterPlayer() { 112 if (!USE_AUDIOFLINGER_MUTING_FOR_OP) { 113 IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE); 114 mAppOps = IAppOpsService.Stub.asInterface(b); 115 // initialize mHasAppOpsPlayAudio 116 updateAppOpsPlayAudio(); 117 // register a callback to monitor whether the OP_PLAY_AUDIO is still allowed 118 mAppOpsCallback = new IAppOpsCallbackWrapper(this); 119 try { 120 mAppOps.startWatchingMode(AppOpsManager.OP_PLAY_AUDIO, 121 ActivityThread.currentPackageName(), mAppOpsCallback); 122 } catch (RemoteException e) { 123 Log.e(TAG, "Error registering appOps callback", e); 124 mHasAppOpsPlayAudio = false; 125 } 126 } 127 try { 128 mPlayerIId = getService().trackPlayer( 129 new PlayerIdCard(mImplType, mAttributes, new IPlayerWrapper(this))); 130 } catch (RemoteException e) { 131 Log.e(TAG, "Error talking to audio service, player will not be tracked", e); 132 } 133 } 134 135 /** 136 * To be called whenever the audio attributes of the player change 137 * @param attr non-null audio attributes 138 */ baseUpdateAudioAttributes(@onNull AudioAttributes attr)139 void baseUpdateAudioAttributes(@NonNull AudioAttributes attr) { 140 if (attr == null) { 141 throw new IllegalArgumentException("Illegal null AudioAttributes"); 142 } 143 try { 144 getService().playerAttributes(mPlayerIId, attr); 145 } catch (RemoteException e) { 146 Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e); 147 } 148 synchronized (mLock) { 149 boolean attributesChanged = (mAttributes != attr); 150 mAttributes = attr; 151 updateAppOpsPlayAudio_sync(attributesChanged); 152 } 153 } 154 updateState(int state)155 private void updateState(int state) { 156 final int piid; 157 synchronized (mLock) { 158 mState = state; 159 piid = mPlayerIId; 160 } 161 try { 162 getService().playerEvent(piid, state); 163 } catch (RemoteException e) { 164 Log.e(TAG, "Error talking to audio service, " 165 + AudioPlaybackConfiguration.toLogFriendlyPlayerState(state) 166 + " state will not be tracked for piid=" + piid, e); 167 } 168 } 169 baseStart()170 void baseStart() { 171 if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); } 172 updateState(AudioPlaybackConfiguration.PLAYER_STATE_STARTED); 173 synchronized (mLock) { 174 if (isRestricted_sync()) { 175 playerSetVolume(true/*muting*/,0, 0); 176 } 177 } 178 } 179 baseSetStartDelayMs(int delayMs)180 void baseSetStartDelayMs(int delayMs) { 181 synchronized(mLock) { 182 mStartDelayMs = Math.max(delayMs, 0); 183 } 184 } 185 getStartDelayMs()186 protected int getStartDelayMs() { 187 synchronized(mLock) { 188 return mStartDelayMs; 189 } 190 } 191 basePause()192 void basePause() { 193 if (DEBUG) { Log.v(TAG, "basePause() piid=" + mPlayerIId); } 194 updateState(AudioPlaybackConfiguration.PLAYER_STATE_PAUSED); 195 } 196 baseStop()197 void baseStop() { 198 if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); } 199 updateState(AudioPlaybackConfiguration.PLAYER_STATE_STOPPED); 200 } 201 baseSetPan(float pan)202 void baseSetPan(float pan) { 203 final float p = Math.min(Math.max(-1.0f, pan), 1.0f); 204 synchronized (mLock) { 205 if (p >= 0.0f) { 206 mPanMultiplierL = 1.0f - p; 207 mPanMultiplierR = 1.0f; 208 } else { 209 mPanMultiplierL = 1.0f; 210 mPanMultiplierR = 1.0f + p; 211 } 212 } 213 updatePlayerVolume(); 214 } 215 updatePlayerVolume()216 private void updatePlayerVolume() { 217 final float finalLeftVol, finalRightVol; 218 final boolean isRestricted; 219 synchronized (mLock) { 220 finalLeftVol = mVolMultiplier * mLeftVolume * mPanMultiplierL; 221 finalRightVol = mVolMultiplier * mRightVolume * mPanMultiplierR; 222 isRestricted = isRestricted_sync(); 223 } 224 playerSetVolume(isRestricted /*muting*/, finalLeftVol, finalRightVol); 225 } 226 setVolumeMultiplier(float vol)227 void setVolumeMultiplier(float vol) { 228 synchronized (mLock) { 229 this.mVolMultiplier = vol; 230 } 231 updatePlayerVolume(); 232 } 233 baseSetVolume(float leftVolume, float rightVolume)234 void baseSetVolume(float leftVolume, float rightVolume) { 235 synchronized (mLock) { 236 mLeftVolume = leftVolume; 237 mRightVolume = rightVolume; 238 } 239 updatePlayerVolume(); 240 } 241 baseSetAuxEffectSendLevel(float level)242 int baseSetAuxEffectSendLevel(float level) { 243 synchronized (mLock) { 244 mAuxEffectSendLevel = level; 245 if (isRestricted_sync()) { 246 return AudioSystem.SUCCESS; 247 } 248 } 249 return playerSetAuxEffectSendLevel(false/*muting*/, level); 250 } 251 252 /** 253 * To be called from a subclass release or finalize method. 254 * Releases AppOps related resources. 255 */ baseRelease()256 void baseRelease() { 257 if (DEBUG) { Log.v(TAG, "baseRelease() piid=" + mPlayerIId + " state=" + mState); } 258 boolean releasePlayer = false; 259 synchronized (mLock) { 260 if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) { 261 releasePlayer = true; 262 mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED; 263 } 264 } 265 try { 266 if (releasePlayer) { 267 getService().releasePlayer(mPlayerIId); 268 } 269 } catch (RemoteException e) { 270 Log.e(TAG, "Error talking to audio service, the player will still be tracked", e); 271 } 272 try { 273 if (mAppOps != null) { 274 mAppOps.stopWatchingMode(mAppOpsCallback); 275 } 276 } catch (Exception e) { 277 // nothing to do here, the object is supposed to be released anyway 278 } 279 } 280 updateAppOpsPlayAudio()281 private void updateAppOpsPlayAudio() { 282 synchronized (mLock) { 283 updateAppOpsPlayAudio_sync(false); 284 } 285 } 286 287 /** 288 * To be called whenever a condition that might affect audibility of this player is updated. 289 * Must be called synchronized on mLock. 290 */ updateAppOpsPlayAudio_sync(boolean attributesChanged)291 void updateAppOpsPlayAudio_sync(boolean attributesChanged) { 292 if (USE_AUDIOFLINGER_MUTING_FOR_OP) { 293 return; 294 } 295 boolean oldHasAppOpsPlayAudio = mHasAppOpsPlayAudio; 296 try { 297 int mode = AppOpsManager.MODE_IGNORED; 298 if (mAppOps != null) { 299 mode = mAppOps.checkAudioOperation(AppOpsManager.OP_PLAY_AUDIO, 300 mAttributes.getUsage(), 301 Process.myUid(), ActivityThread.currentPackageName()); 302 } 303 mHasAppOpsPlayAudio = (mode == AppOpsManager.MODE_ALLOWED); 304 } catch (RemoteException e) { 305 mHasAppOpsPlayAudio = false; 306 } 307 308 // AppsOps alters a player's volume; when the restriction changes, reflect it on the actual 309 // volume used by the player 310 try { 311 if (oldHasAppOpsPlayAudio != mHasAppOpsPlayAudio || 312 attributesChanged) { 313 getService().playerHasOpPlayAudio(mPlayerIId, mHasAppOpsPlayAudio); 314 if (!isRestricted_sync()) { 315 if (DEBUG_APP_OPS) { 316 Log.v(TAG, "updateAppOpsPlayAudio: unmuting player, vol=" + mLeftVolume 317 + "/" + mRightVolume); 318 } 319 playerSetVolume(false/*muting*/, 320 mLeftVolume * mPanMultiplierL, mRightVolume * mPanMultiplierR); 321 playerSetAuxEffectSendLevel(false/*muting*/, mAuxEffectSendLevel); 322 } else { 323 if (DEBUG_APP_OPS) { 324 Log.v(TAG, "updateAppOpsPlayAudio: muting player"); 325 } 326 playerSetVolume(true/*muting*/, 0.0f, 0.0f); 327 playerSetAuxEffectSendLevel(true/*muting*/, 0.0f); 328 } 329 } 330 } catch (Exception e) { 331 // failing silently, player might not be in right state 332 } 333 } 334 335 /** 336 * To be called by the subclass whenever an operation is potentially restricted. 337 * As the media player-common behavior are incorporated into this class, the subclass's need 338 * to call this method should be removed, and this method could become private. 339 * FIXME can this method be private so subclasses don't have to worry about when to check 340 * the restrictions. 341 * @return 342 */ isRestricted_sync()343 boolean isRestricted_sync() { 344 if (USE_AUDIOFLINGER_MUTING_FOR_OP) { 345 return false; 346 } 347 // check app ops 348 if (mHasAppOpsPlayAudio) { 349 return false; 350 } 351 // check bypass flag 352 if ((mAttributes.getAllFlags() & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0) { 353 return false; 354 } 355 // check force audibility flag and camera restriction 356 if (((mAttributes.getAllFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED) != 0) 357 && (mAttributes.getUsage() == AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)) { 358 boolean cameraSoundForced = false; 359 try { 360 cameraSoundForced = getService().isCameraSoundForced(); 361 } catch (RemoteException e) { 362 Log.e(TAG, "Cannot access AudioService in isRestricted_sync()"); 363 } catch (NullPointerException e) { 364 Log.e(TAG, "Null AudioService in isRestricted_sync()"); 365 } 366 if (cameraSoundForced) { 367 return false; 368 } 369 } 370 return true; 371 } 372 getService()373 private static IAudioService getService() 374 { 375 if (sService != null) { 376 return sService; 377 } 378 IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); 379 sService = IAudioService.Stub.asInterface(b); 380 return sService; 381 } 382 383 /** 384 * @hide 385 * @param delayMs 386 */ setStartDelayMs(int delayMs)387 public void setStartDelayMs(int delayMs) { 388 baseSetStartDelayMs(delayMs); 389 } 390 391 //===================================================================== 392 // Abstract methods a subclass needs to implement 393 /** 394 * Abstract method for the subclass behavior's for volume and muting commands 395 * @param muting if true, the player is to be muted, and the volume values can be ignored 396 * @param leftVolume the left volume to use if muting is false 397 * @param rightVolume the right volume to use if muting is false 398 */ playerSetVolume(boolean muting, float leftVolume, float rightVolume)399 abstract void playerSetVolume(boolean muting, float leftVolume, float rightVolume); 400 401 /** 402 * Abstract method to apply a {@link VolumeShaper.Configuration} 403 * and a {@link VolumeShaper.Operation} to the Player. 404 * This should be overridden by the Player to call into the native 405 * VolumeShaper implementation. Multiple {@code VolumeShapers} may be 406 * concurrently active for a given Player, each accessible by the 407 * {@code VolumeShaper} id. 408 * 409 * The {@code VolumeShaper} implementation caches the id returned 410 * when applying a fully specified configuration 411 * from {VolumeShaper.Configuration.Builder} to track later 412 * operation changes requested on it. 413 * 414 * @param configuration a {@code VolumeShaper.Configuration} object 415 * created by {@link VolumeShaper.Configuration.Builder} or 416 * an created from a {@code VolumeShaper} id 417 * by the {@link VolumeShaper.Configuration} constructor. 418 * @param operation a {@code VolumeShaper.Operation}. 419 * @return a negative error status or a 420 * non-negative {@code VolumeShaper} id on success. 421 */ playerApplyVolumeShaper( @onNull VolumeShaper.Configuration configuration, @NonNull VolumeShaper.Operation operation)422 /* package */ abstract int playerApplyVolumeShaper( 423 @NonNull VolumeShaper.Configuration configuration, 424 @NonNull VolumeShaper.Operation operation); 425 426 /** 427 * Abstract method to get the current VolumeShaper state. 428 * @param id the {@code VolumeShaper} id returned from 429 * sending a fully specified {@code VolumeShaper.Configuration} 430 * through {@link #playerApplyVolumeShaper} 431 * @return a {@code VolumeShaper.State} object or null if 432 * there is no {@code VolumeShaper} for the id. 433 */ playerGetVolumeShaperState(int id)434 /* package */ abstract @Nullable VolumeShaper.State playerGetVolumeShaperState(int id); 435 playerSetAuxEffectSendLevel(boolean muting, float level)436 abstract int playerSetAuxEffectSendLevel(boolean muting, float level); playerStart()437 abstract void playerStart(); playerPause()438 abstract void playerPause(); playerStop()439 abstract void playerStop(); 440 441 //===================================================================== 442 private static class IAppOpsCallbackWrapper extends IAppOpsCallback.Stub { 443 private final WeakReference<PlayerBase> mWeakPB; 444 IAppOpsCallbackWrapper(PlayerBase pb)445 public IAppOpsCallbackWrapper(PlayerBase pb) { 446 mWeakPB = new WeakReference<PlayerBase>(pb); 447 } 448 449 @Override opChanged(int op, int uid, String packageName)450 public void opChanged(int op, int uid, String packageName) { 451 if (op == AppOpsManager.OP_PLAY_AUDIO) { 452 if (DEBUG_APP_OPS) { Log.v(TAG, "opChanged: op=PLAY_AUDIO pack=" + packageName); } 453 final PlayerBase pb = mWeakPB.get(); 454 if (pb != null) { 455 pb.updateAppOpsPlayAudio(); 456 } 457 } 458 } 459 } 460 461 //===================================================================== 462 /** 463 * Wrapper around an implementation of IPlayer for all subclasses of PlayerBase 464 * that doesn't keep a strong reference on PlayerBase 465 */ 466 private static class IPlayerWrapper extends IPlayer.Stub { 467 private final WeakReference<PlayerBase> mWeakPB; 468 IPlayerWrapper(PlayerBase pb)469 public IPlayerWrapper(PlayerBase pb) { 470 mWeakPB = new WeakReference<PlayerBase>(pb); 471 } 472 473 @Override start()474 public void start() { 475 final PlayerBase pb = mWeakPB.get(); 476 if (pb != null) { 477 pb.playerStart(); 478 } 479 } 480 481 @Override pause()482 public void pause() { 483 final PlayerBase pb = mWeakPB.get(); 484 if (pb != null) { 485 pb.playerPause(); 486 } 487 } 488 489 @Override stop()490 public void stop() { 491 final PlayerBase pb = mWeakPB.get(); 492 if (pb != null) { 493 pb.playerStop(); 494 } 495 } 496 497 @Override setVolume(float vol)498 public void setVolume(float vol) { 499 final PlayerBase pb = mWeakPB.get(); 500 if (pb != null) { 501 pb.setVolumeMultiplier(vol); 502 } 503 } 504 505 @Override setPan(float pan)506 public void setPan(float pan) { 507 final PlayerBase pb = mWeakPB.get(); 508 if (pb != null) { 509 pb.baseSetPan(pan); 510 } 511 } 512 513 @Override setStartDelayMs(int delayMs)514 public void setStartDelayMs(int delayMs) { 515 final PlayerBase pb = mWeakPB.get(); 516 if (pb != null) { 517 pb.baseSetStartDelayMs(delayMs); 518 } 519 } 520 521 @Override applyVolumeShaper( @onNull VolumeShaper.Configuration configuration, @NonNull VolumeShaper.Operation operation)522 public void applyVolumeShaper( 523 @NonNull VolumeShaper.Configuration configuration, 524 @NonNull VolumeShaper.Operation operation) { 525 final PlayerBase pb = mWeakPB.get(); 526 if (pb != null) { 527 pb.playerApplyVolumeShaper(configuration, operation); 528 } 529 } 530 } 531 532 //===================================================================== 533 /** 534 * Class holding all the information about a player that needs to be known at registration time 535 */ 536 public static class PlayerIdCard implements Parcelable { 537 public final int mPlayerType; 538 539 public static final int AUDIO_ATTRIBUTES_NONE = 0; 540 public static final int AUDIO_ATTRIBUTES_DEFINED = 1; 541 public final AudioAttributes mAttributes; 542 public final IPlayer mIPlayer; 543 PlayerIdCard(int type, @NonNull AudioAttributes attr, @NonNull IPlayer iplayer)544 PlayerIdCard(int type, @NonNull AudioAttributes attr, @NonNull IPlayer iplayer) { 545 mPlayerType = type; 546 mAttributes = attr; 547 mIPlayer = iplayer; 548 } 549 550 @Override hashCode()551 public int hashCode() { 552 return Objects.hash(mPlayerType); 553 } 554 555 @Override describeContents()556 public int describeContents() { 557 return 0; 558 } 559 560 @Override writeToParcel(Parcel dest, int flags)561 public void writeToParcel(Parcel dest, int flags) { 562 dest.writeInt(mPlayerType); 563 mAttributes.writeToParcel(dest, 0); 564 dest.writeStrongBinder(mIPlayer == null ? null : mIPlayer.asBinder()); 565 } 566 567 public static final @android.annotation.NonNull Parcelable.Creator<PlayerIdCard> CREATOR 568 = new Parcelable.Creator<PlayerIdCard>() { 569 /** 570 * Rebuilds an PlayerIdCard previously stored with writeToParcel(). 571 * @param p Parcel object to read the PlayerIdCard from 572 * @return a new PlayerIdCard created from the data in the parcel 573 */ 574 public PlayerIdCard createFromParcel(Parcel p) { 575 return new PlayerIdCard(p); 576 } 577 public PlayerIdCard[] newArray(int size) { 578 return new PlayerIdCard[size]; 579 } 580 }; 581 PlayerIdCard(Parcel in)582 private PlayerIdCard(Parcel in) { 583 mPlayerType = in.readInt(); 584 mAttributes = AudioAttributes.CREATOR.createFromParcel(in); 585 // IPlayer can be null if unmarshalling a Parcel coming from who knows where 586 final IBinder b = in.readStrongBinder(); 587 mIPlayer = (b == null ? null : IPlayer.Stub.asInterface(b)); 588 } 589 590 @Override equals(Object o)591 public boolean equals(Object o) { 592 if (this == o) return true; 593 if (o == null || !(o instanceof PlayerIdCard)) return false; 594 595 PlayerIdCard that = (PlayerIdCard) o; 596 597 // FIXME change to the binder player interface once supported as a member 598 return ((mPlayerType == that.mPlayerType) && mAttributes.equals(that.mAttributes)); 599 } 600 } 601 602 //===================================================================== 603 // Utilities 604 605 /** 606 * @hide 607 * Use to generate warning or exception in legacy code paths that allowed passing stream types 608 * to qualify audio playback. 609 * @param streamType the stream type to check 610 * @throws IllegalArgumentException 611 */ deprecateStreamTypeForPlayback(int streamType, @NonNull String className, @NonNull String opName)612 public static void deprecateStreamTypeForPlayback(int streamType, @NonNull String className, 613 @NonNull String opName) throws IllegalArgumentException { 614 // STREAM_ACCESSIBILITY was introduced at the same time the use of stream types 615 // for audio playback was deprecated, so it is not allowed at all to qualify a playback 616 // use case 617 if (streamType == AudioManager.STREAM_ACCESSIBILITY) { 618 throw new IllegalArgumentException("Use of STREAM_ACCESSIBILITY is reserved for " 619 + "volume control"); 620 } 621 Log.w(className, "Use of stream types is deprecated for operations other than " + 622 "volume control"); 623 Log.w(className, "See the documentation of " + opName + " for what to use instead with " + 624 "android.media.AudioAttributes to qualify your playback use case"); 625 } 626 getCurrentOpPackageName()627 protected String getCurrentOpPackageName() { 628 return TextUtils.emptyIfNull(ActivityThread.currentOpPackageName()); 629 } 630 } 631