Home | History | Annotate | Download | only in incallui
      1 /*
      2  * Copyright (C) 2013 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 com.android.incallui;
     18 
     19 import android.content.Context;
     20 import android.os.Bundle;
     21 import android.os.Trace;
     22 import android.support.v4.app.Fragment;
     23 import android.support.v4.os.UserManagerCompat;
     24 import android.telecom.CallAudioState;
     25 import android.telecom.PhoneAccountHandle;
     26 import com.android.contacts.common.compat.CallCompat;
     27 import com.android.dialer.common.Assert;
     28 import com.android.dialer.common.LogUtil;
     29 import com.android.dialer.common.concurrent.DialerExecutorComponent;
     30 import com.android.dialer.logging.DialerImpression;
     31 import com.android.dialer.logging.DialerImpression.Type;
     32 import com.android.dialer.logging.Logger;
     33 import com.android.dialer.telecom.TelecomUtil;
     34 import com.android.incallui.InCallCameraManager.Listener;
     35 import com.android.incallui.InCallPresenter.CanAddCallListener;
     36 import com.android.incallui.InCallPresenter.InCallDetailsListener;
     37 import com.android.incallui.InCallPresenter.InCallState;
     38 import com.android.incallui.InCallPresenter.InCallStateListener;
     39 import com.android.incallui.InCallPresenter.IncomingCallListener;
     40 import com.android.incallui.audiomode.AudioModeProvider;
     41 import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
     42 import com.android.incallui.call.CallList;
     43 import com.android.incallui.call.DialerCall;
     44 import com.android.incallui.call.DialerCall.CameraDirection;
     45 import com.android.incallui.call.TelecomAdapter;
     46 import com.android.incallui.incall.protocol.InCallButtonIds;
     47 import com.android.incallui.incall.protocol.InCallButtonUi;
     48 import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
     49 import com.android.incallui.multisim.SwapSimWorker;
     50 import com.android.incallui.videotech.utils.VideoUtils;
     51 
     52 /** Logic for call buttons. */
     53 public class CallButtonPresenter
     54     implements InCallStateListener,
     55         AudioModeListener,
     56         IncomingCallListener,
     57         InCallDetailsListener,
     58         CanAddCallListener,
     59         Listener,
     60         InCallButtonUiDelegate {
     61 
     62   private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted";
     63   private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state";
     64 
     65   private final Context context;
     66   private InCallButtonUi inCallButtonUi;
     67   private DialerCall call;
     68   private boolean automaticallyMuted = false;
     69   private boolean previousMuteState = false;
     70   private boolean isInCallButtonUiReady;
     71   private PhoneAccountHandle otherAccount;
     72 
     73   public CallButtonPresenter(Context context) {
     74     this.context = context.getApplicationContext();
     75   }
     76 
     77   @Override
     78   public void onInCallButtonUiReady(InCallButtonUi ui) {
     79     Assert.checkState(!isInCallButtonUiReady);
     80     inCallButtonUi = ui;
     81     AudioModeProvider.getInstance().addListener(this);
     82 
     83     // register for call state changes last
     84     final InCallPresenter inCallPresenter = InCallPresenter.getInstance();
     85     inCallPresenter.addListener(this);
     86     inCallPresenter.addIncomingCallListener(this);
     87     inCallPresenter.addDetailsListener(this);
     88     inCallPresenter.addCanAddCallListener(this);
     89     inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this);
     90 
     91     // Update the buttons state immediately for the current call
     92     onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), CallList.getInstance());
     93     isInCallButtonUiReady = true;
     94   }
     95 
     96   @Override
     97   public void onInCallButtonUiUnready() {
     98     Assert.checkState(isInCallButtonUiReady);
     99     inCallButtonUi = null;
    100     InCallPresenter.getInstance().removeListener(this);
    101     AudioModeProvider.getInstance().removeListener(this);
    102     InCallPresenter.getInstance().removeIncomingCallListener(this);
    103     InCallPresenter.getInstance().removeDetailsListener(this);
    104     InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
    105     InCallPresenter.getInstance().removeCanAddCallListener(this);
    106     isInCallButtonUiReady = false;
    107   }
    108 
    109   @Override
    110   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
    111     Trace.beginSection("CallButtonPresenter.onStateChange");
    112     if (newState == InCallState.OUTGOING) {
    113       call = callList.getOutgoingCall();
    114     } else if (newState == InCallState.INCALL) {
    115       call = callList.getActiveOrBackgroundCall();
    116 
    117       // When connected to voice mail, automatically shows the dialpad.
    118       // (On previous releases we showed it when in-call shows up, before waiting for
    119       // OUTGOING.  We may want to do that once we start showing "Voice mail" label on
    120       // the dialpad too.)
    121       if (oldState == InCallState.OUTGOING && call != null) {
    122         if (call.isVoiceMailNumber() && getActivity() != null) {
    123           getActivity().showDialpadFragment(true /* show */, true /* animate */);
    124         }
    125       }
    126     } else if (newState == InCallState.INCOMING) {
    127       if (getActivity() != null) {
    128         getActivity().showDialpadFragment(false /* show */, true /* animate */);
    129       }
    130       call = callList.getIncomingCall();
    131     } else {
    132       call = null;
    133     }
    134     updateUi(newState, call);
    135     Trace.endSection();
    136   }
    137 
    138   /**
    139    * Updates the user interface in response to a change in the details of a call. Currently handles
    140    * changes to the call buttons in response to a change in the details for a call. This is
    141    * important to ensure changes to the active call are reflected in the available buttons.
    142    *
    143    * @param call The active call.
    144    * @param details The call details.
    145    */
    146   @Override
    147   public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
    148     // Only update if the changes are for the currently active call
    149     if (inCallButtonUi != null && call != null && call.equals(this.call)) {
    150       updateButtonsState(call);
    151     }
    152   }
    153 
    154   @Override
    155   public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
    156     onStateChange(oldState, newState, CallList.getInstance());
    157   }
    158 
    159   @Override
    160   public void onCanAddCallChanged(boolean canAddCall) {
    161     if (inCallButtonUi != null && call != null) {
    162       updateButtonsState(call);
    163     }
    164   }
    165 
    166   @Override
    167   public void onAudioStateChanged(CallAudioState audioState) {
    168     if (inCallButtonUi != null) {
    169       inCallButtonUi.setAudioState(audioState);
    170     }
    171   }
    172 
    173   @Override
    174   public CallAudioState getCurrentAudioState() {
    175     return AudioModeProvider.getInstance().getAudioState();
    176   }
    177 
    178   @Override
    179   public void setAudioRoute(int route) {
    180     LogUtil.i(
    181         "CallButtonPresenter.setAudioRoute",
    182         "sending new audio route: " + CallAudioState.audioRouteToString(route));
    183     TelecomAdapter.getInstance().setAudioRoute(route);
    184   }
    185 
    186   /** Function assumes that bluetooth is not supported. */
    187   @Override
    188   public void toggleSpeakerphone() {
    189     // This function should not be called if bluetooth is available.
    190     CallAudioState audioState = getCurrentAudioState();
    191     if (0 != (CallAudioState.ROUTE_BLUETOOTH & audioState.getSupportedRouteMask())) {
    192       // It's clear the UI is wrong, so update the supported mode once again.
    193       LogUtil.e(
    194           "CallButtonPresenter", "toggling speakerphone not allowed when bluetooth supported.");
    195       inCallButtonUi.setAudioState(audioState);
    196       return;
    197     }
    198 
    199     int newRoute;
    200     if (audioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
    201       newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
    202       Logger.get(context)
    203           .logCallImpression(
    204               DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE,
    205               call.getUniqueCallId(),
    206               call.getTimeAddedMs());
    207     } else {
    208       newRoute = CallAudioState.ROUTE_SPEAKER;
    209       Logger.get(context)
    210           .logCallImpression(
    211               DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_SPEAKERPHONE,
    212               call.getUniqueCallId(),
    213               call.getTimeAddedMs());
    214     }
    215 
    216     setAudioRoute(newRoute);
    217   }
    218 
    219   @Override
    220   public void muteClicked(boolean checked, boolean clickedByUser) {
    221     LogUtil.i(
    222         "CallButtonPresenter", "turning on mute: %s, clicked by user: %s", checked, clickedByUser);
    223     if (clickedByUser) {
    224       Logger.get(context)
    225           .logCallImpression(
    226               checked
    227                   ? DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_MUTE
    228                   : DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_MUTE,
    229               call.getUniqueCallId(),
    230               call.getTimeAddedMs());
    231     }
    232     TelecomAdapter.getInstance().mute(checked);
    233   }
    234 
    235   @Override
    236   public void holdClicked(boolean checked) {
    237     if (call == null) {
    238       return;
    239     }
    240     if (checked) {
    241       LogUtil.i("CallButtonPresenter", "putting the call on hold: " + call);
    242       call.hold();
    243     } else {
    244       LogUtil.i("CallButtonPresenter", "removing the call from hold: " + call);
    245       call.unhold();
    246     }
    247   }
    248 
    249   @Override
    250   public void swapClicked() {
    251     if (call == null) {
    252       return;
    253     }
    254 
    255     LogUtil.i("CallButtonPresenter", "swapping the call: " + call);
    256     TelecomAdapter.getInstance().swap(call.getId());
    257   }
    258 
    259   @Override
    260   public void mergeClicked() {
    261     Logger.get(context)
    262         .logCallImpression(
    263             DialerImpression.Type.IN_CALL_MERGE_BUTTON_PRESSED,
    264             call.getUniqueCallId(),
    265             call.getTimeAddedMs());
    266     TelecomAdapter.getInstance().merge(call.getId());
    267   }
    268 
    269   @Override
    270   public void addCallClicked() {
    271     Logger.get(context)
    272         .logCallImpression(
    273             DialerImpression.Type.IN_CALL_ADD_CALL_BUTTON_PRESSED,
    274             call.getUniqueCallId(),
    275             call.getTimeAddedMs());
    276     // Automatically mute the current call
    277     automaticallyMuted = true;
    278     previousMuteState = AudioModeProvider.getInstance().getAudioState().isMuted();
    279     // Simulate a click on the mute button
    280     muteClicked(true /* checked */, false /* clickedByUser */);
    281     TelecomAdapter.getInstance().addCall();
    282   }
    283 
    284   @Override
    285   public void showDialpadClicked(boolean checked) {
    286     Logger.get(context)
    287         .logCallImpression(
    288             DialerImpression.Type.IN_CALL_SHOW_DIALPAD_BUTTON_PRESSED,
    289             call.getUniqueCallId(),
    290             call.getTimeAddedMs());
    291     LogUtil.v("CallButtonPresenter", "show dialpad " + String.valueOf(checked));
    292     getActivity().showDialpadFragment(checked /* show */, true /* animate */);
    293   }
    294 
    295   @Override
    296   public void changeToVideoClicked() {
    297     LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
    298     Logger.get(context)
    299         .logCallImpression(
    300             DialerImpression.Type.VIDEO_CALL_UPGRADE_REQUESTED,
    301             call.getUniqueCallId(),
    302             call.getTimeAddedMs());
    303     call.getVideoTech().upgradeToVideo(context);
    304   }
    305 
    306   @Override
    307   public void onEndCallClicked() {
    308     LogUtil.i("CallButtonPresenter.onEndCallClicked", "call: " + call);
    309     if (call != null) {
    310       call.disconnect();
    311     }
    312   }
    313 
    314   @Override
    315   public void showAudioRouteSelector() {
    316     inCallButtonUi.showAudioRouteSelector();
    317   }
    318 
    319   @Override
    320   public void swapSimClicked() {
    321     LogUtil.enterBlock("CallButtonPresenter.swapSimClicked");
    322     Logger.get(getContext()).logImpression(Type.DUAL_SIM_CHANGE_SIM_PRESSED);
    323     SwapSimWorker worker =
    324         new SwapSimWorker(
    325             getContext(),
    326             call,
    327             InCallPresenter.getInstance().getCallList(),
    328             otherAccount,
    329             InCallPresenter.getInstance().acquireInCallUiLock("swapSim"));
    330     DialerExecutorComponent.get(getContext())
    331         .dialerExecutorFactory()
    332         .createNonUiTaskBuilder(worker)
    333         .build()
    334         .executeParallel(null);
    335   }
    336 
    337   /**
    338    * Switches the camera between the front-facing and back-facing camera.
    339    *
    340    * @param useFrontFacingCamera True if we should switch to using the front-facing camera, or false
    341    *     if we should switch to using the back-facing camera.
    342    */
    343   @Override
    344   public void switchCameraClicked(boolean useFrontFacingCamera) {
    345     updateCamera(useFrontFacingCamera);
    346   }
    347 
    348   @Override
    349   public void toggleCameraClicked() {
    350     LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
    351     if (call == null) {
    352       return;
    353     }
    354     Logger.get(context)
    355         .logCallImpression(
    356             DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA,
    357             call.getUniqueCallId(),
    358             call.getTimeAddedMs());
    359     switchCameraClicked(
    360         !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
    361   }
    362 
    363   /**
    364    * Stop or start client's video transmission.
    365    *
    366    * @param pause True if pausing the local user's video, or false if starting the local user's
    367    *     video.
    368    */
    369   @Override
    370   public void pauseVideoClicked(boolean pause) {
    371     LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
    372 
    373     Logger.get(context)
    374         .logCallImpression(
    375             pause
    376                 ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO
    377                 : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO,
    378             call.getUniqueCallId(),
    379             call.getTimeAddedMs());
    380 
    381     if (pause) {
    382       call.getVideoTech().setCamera(null);
    383       call.getVideoTech().stopTransmission();
    384     } else {
    385       updateCamera(
    386           InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
    387       call.getVideoTech().resumeTransmission(context);
    388     }
    389 
    390     inCallButtonUi.setVideoPaused(pause);
    391     inCallButtonUi.enableButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, false);
    392   }
    393 
    394   private void updateCamera(boolean useFrontFacingCamera) {
    395     InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
    396     cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
    397 
    398     String cameraId = cameraManager.getActiveCameraId();
    399     if (cameraId != null) {
    400       final int cameraDir =
    401           cameraManager.isUsingFrontFacingCamera()
    402               ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING
    403               : CameraDirection.CAMERA_DIRECTION_BACK_FACING;
    404       call.setCameraDir(cameraDir);
    405       call.getVideoTech().setCamera(cameraId);
    406     }
    407   }
    408 
    409   private void updateUi(InCallState state, DialerCall call) {
    410     LogUtil.v("CallButtonPresenter", "updating call UI for call: %s", call);
    411 
    412     if (inCallButtonUi == null) {
    413       return;
    414     }
    415 
    416     if (call != null) {
    417       inCallButtonUi.updateInCallButtonUiColors(
    418           InCallPresenter.getInstance().getThemeColorManager().getSecondaryColor());
    419     }
    420 
    421     final boolean isEnabled =
    422         state.isConnectingOrConnected() && !state.isIncoming() && call != null;
    423     inCallButtonUi.setEnabled(isEnabled);
    424 
    425     if (call == null) {
    426       return;
    427     }
    428 
    429     updateButtonsState(call);
    430   }
    431 
    432   /**
    433    * Updates the buttons applicable for the UI.
    434    *
    435    * @param call The active call.
    436    */
    437   @SuppressWarnings("MissingPermission")
    438   private void updateButtonsState(DialerCall call) {
    439     LogUtil.v("CallButtonPresenter.updateButtonsState", "");
    440     final boolean isVideo = call.isVideoCall();
    441 
    442     // Common functionality (audio, hold, etc).
    443     // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
    444     //     (1) If the device normally can hold, show HOLD in a disabled state.
    445     //     (2) If the device doesn't have the concept of hold/swap, remove the button.
    446     final boolean showSwap = call.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
    447     final boolean showHold =
    448         !showSwap
    449             && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD)
    450             && call.can(android.telecom.Call.Details.CAPABILITY_HOLD);
    451     final boolean isCallOnHold = call.getState() == DialerCall.State.ONHOLD;
    452 
    453     final boolean showAddCall =
    454         TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(context);
    455     final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
    456     final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call));
    457     final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
    458     final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
    459 
    460     final boolean hasCameraPermission =
    461         isVideo && VideoUtils.hasCameraPermissionAndShownPrivacyToast(context);
    462     // Disabling local video doesn't seem to work when dialing. See a bug.
    463     final boolean showPauseVideo =
    464         isVideo
    465             && call.getState() != DialerCall.State.DIALING
    466             && call.getState() != DialerCall.State.CONNECTING;
    467 
    468     otherAccount = TelecomUtil.getOtherAccount(getContext(), call.getAccountHandle());
    469     boolean showSwapSim =
    470         otherAccount != null
    471             && !call.isVoiceMailNumber()
    472             && DialerCall.State.isDialing(call.getState())
    473             // Most devices cannot make calls on 2 SIMs at the same time.
    474             && InCallPresenter.getInstance().getCallList().getAllCalls().size() == 1;
    475 
    476     inCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true);
    477     inCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap);
    478     inCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold);
    479     inCallButtonUi.setHold(isCallOnHold);
    480     inCallButtonUi.showButton(InCallButtonIds.BUTTON_MUTE, showMute);
    481     inCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP_SIM, showSwapSim);
    482     inCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true);
    483     inCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall);
    484     inCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo);
    485     inCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio);
    486     inCallButtonUi.showButton(
    487         InCallButtonIds.BUTTON_SWITCH_CAMERA,
    488         isVideo && hasCameraPermission && call.getVideoTech().isTransmitting());
    489     inCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
    490     if (isVideo) {
    491       inCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission);
    492     }
    493     inCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
    494     inCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
    495 
    496     inCallButtonUi.updateButtonStates();
    497   }
    498 
    499   private boolean hasVideoCallCapabilities(DialerCall call) {
    500     return call.getVideoTech().isAvailable(context, call.getAccountHandle());
    501   }
    502 
    503   /**
    504    * Determines if downgrading from a video call to an audio-only call is supported. In order to
    505    * support downgrade to audio, the SDK version must be >= N and the call should NOT have the
    506    * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}.
    507    *
    508    * @param call The call.
    509    * @return {@code true} if downgrading to an audio-only call from a video call is supported.
    510    */
    511   private boolean isDowngradeToAudioSupported(DialerCall call) {
    512     // TODO(a bug): If there is an RCS video share session, return true here
    513     return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
    514   }
    515 
    516   @Override
    517   public void refreshMuteState() {
    518     // Restore the previous mute state
    519     if (automaticallyMuted
    520         && AudioModeProvider.getInstance().getAudioState().isMuted() != previousMuteState) {
    521       if (inCallButtonUi == null) {
    522         return;
    523       }
    524       muteClicked(previousMuteState, false /* clickedByUser */);
    525     }
    526     automaticallyMuted = false;
    527   }
    528 
    529   @Override
    530   public void onSaveInstanceState(Bundle outState) {
    531     outState.putBoolean(KEY_AUTOMATICALLY_MUTED, automaticallyMuted);
    532     outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, previousMuteState);
    533   }
    534 
    535   @Override
    536   public void onRestoreInstanceState(Bundle savedInstanceState) {
    537     automaticallyMuted = savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, automaticallyMuted);
    538     previousMuteState = savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, previousMuteState);
    539   }
    540 
    541   @Override
    542   public void onCameraPermissionGranted() {
    543     if (call != null) {
    544       updateButtonsState(call);
    545     }
    546   }
    547 
    548   @Override
    549   public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) {
    550     if (inCallButtonUi == null) {
    551       return;
    552     }
    553     inCallButtonUi.setCameraSwitched(!isUsingFrontFacingCamera);
    554   }
    555 
    556   @Override
    557   public Context getContext() {
    558     return context;
    559   }
    560 
    561   private InCallActivity getActivity() {
    562     if (inCallButtonUi != null) {
    563       Fragment fragment = inCallButtonUi.getInCallButtonUiFragment();
    564       if (fragment != null) {
    565         return (InCallActivity) fragment.getActivity();
    566       }
    567     }
    568     return null;
    569   }
    570 }
    571