/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.incallui.rtt.impl; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.OnScrollListener; import android.telecom.CallAudioState; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.EditorInfo; import android.widget.Chronometer; import android.widget.EditText; import android.widget.ImageButton; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import com.android.dialer.common.Assert; import com.android.dialer.common.FragmentUtils; import com.android.dialer.common.LogUtil; import com.android.dialer.common.UiUtil; import com.android.dialer.lettertile.LetterTileDrawable; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.rtt.RttTranscript; import com.android.dialer.rtt.RttTranscriptMessage; import com.android.dialer.util.DrawableConverter; import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; import com.android.incallui.call.state.DialerCallState; import com.android.incallui.hold.OnHoldFragment; import com.android.incallui.incall.protocol.ContactPhotoType; import com.android.incallui.incall.protocol.InCallButtonIds; import com.android.incallui.incall.protocol.InCallButtonUi; import com.android.incallui.incall.protocol.InCallButtonUiDelegate; import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory; import com.android.incallui.incall.protocol.InCallScreen; import com.android.incallui.incall.protocol.InCallScreenDelegate; import com.android.incallui.incall.protocol.InCallScreenDelegateFactory; import com.android.incallui.incall.protocol.PrimaryCallState; import com.android.incallui.incall.protocol.PrimaryInfo; import com.android.incallui.incall.protocol.SecondaryInfo; import com.android.incallui.rtt.impl.RttChatAdapter.MessageListener; import com.android.incallui.rtt.protocol.Constants; import com.android.incallui.rtt.protocol.RttCallScreen; import com.android.incallui.rtt.protocol.RttCallScreenDelegate; import com.android.incallui.rtt.protocol.RttCallScreenDelegateFactory; import java.util.List; /** RTT chat fragment to show chat bubbles. */ public class RttChatFragment extends Fragment implements OnEditorActionListener, TextWatcher, MessageListener, RttCallScreen, InCallScreen, InCallButtonUi, AudioRouteSelectorPresenter { private static final String ARG_CALL_ID = "call_id"; private RecyclerView recyclerView; private RttChatAdapter adapter; private EditText editText; private ImageButton submitButton; private boolean isClearingInput; private InCallScreenDelegate inCallScreenDelegate; private RttCallScreenDelegate rttCallScreenDelegate; private InCallButtonUiDelegate inCallButtonUiDelegate; private View endCallButton; private TextView nameTextView; private Chronometer chronometer; private boolean isTimerStarted; private RttOverflowMenu overflowMenu; private SecondaryInfo savedSecondaryInfo; private TextView statusBanner; private PrimaryInfo primaryInfo = PrimaryInfo.empty(); private PrimaryCallState primaryCallState = PrimaryCallState.empty(); private boolean isUserScrolling; private boolean shouldAutoScrolling; private AudioSelectMenu audioSelectMenu; /** * Create a new instance of RttChatFragment. * * @param callId call id of the RTT call. * @return new RttChatFragment */ public static RttChatFragment newInstance(String callId) { Bundle bundle = new Bundle(); bundle.putString(ARG_CALL_ID, callId); RttChatFragment instance = new RttChatFragment(); instance.setArguments(bundle); return instance; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); LogUtil.i("RttChatFragment.onCreate", null); inCallButtonUiDelegate = FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class) .newInCallButtonUiDelegate(); if (savedInstanceState != null) { inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState); } inCallScreenDelegate = FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class) .newInCallScreenDelegate(); // Prevent updating local message until UI is ready. isClearingInput = true; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle bundle) { super.onViewCreated(view, bundle); LogUtil.i("RttChatFragment.onViewCreated", null); rttCallScreenDelegate = FragmentUtils.getParentUnsafe(this, RttCallScreenDelegateFactory.class) .newRttCallScreenDelegate(this); rttCallScreenDelegate.initRttCallScreenDelegate(this); inCallScreenDelegate.onInCallScreenDelegateInit(this); inCallScreenDelegate.onInCallScreenReady(); inCallButtonUiDelegate.onInCallButtonUiReady(this); } @Override public List getRttTranscriptMessageList() { return adapter.getRttTranscriptMessageList(); } @Nullable @Override public View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.frag_rtt_chat, container, false); view.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); editText = view.findViewById(R.id.rtt_chat_input); editText.setOnEditorActionListener(this); editText.addTextChangedListener(this); editText.setOnKeyListener( (v, keyCode, event) -> { // This is only triggered when input method doesn't handle delete key, which usually means // the current input box is empty. // On non-English keyboard delete key could be passed here so we still need to check if // the input box is empty. if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN && TextUtils.isEmpty(editText.getText())) { String lastMessage = adapter.retrieveLastLocalMessage(); if (lastMessage != null) { resumeInput(lastMessage); rttCallScreenDelegate.onLocalMessage("\b"); return true; } return false; } return false; }); recyclerView = view.findViewById(R.id.rtt_recycler_view); LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); layoutManager.setStackFromEnd(true); recyclerView.setLayoutManager(layoutManager); recyclerView.setHasFixedSize(false); adapter = new RttChatAdapter(getContext(), this); recyclerView.setAdapter(adapter); recyclerView.addOnScrollListener( new OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int i) { if (i == RecyclerView.SCROLL_STATE_DRAGGING) { isUserScrolling = true; } else if (i == RecyclerView.SCROLL_STATE_IDLE) { isUserScrolling = false; // Auto scrolling for new messages should be resumed if it's scrolled to bottom. shouldAutoScrolling = !recyclerView.canScrollVertically(1); } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (dy < 0 && isUserScrolling) { UiUtil.hideKeyboardFrom(getContext(), editText); } } }); submitButton = view.findViewById(R.id.rtt_chat_submit_button); submitButton.setOnClickListener( v -> { Logger.get(getContext()).logImpression(DialerImpression.Type.RTT_SEND_BUTTON_CLICKED); adapter.submitLocalMessage(); resumeInput(""); rttCallScreenDelegate.onLocalMessage(Constants.BUBBLE_BREAKER); // Auto scrolling for new messages should be resumed since user has submit current // message. shouldAutoScrolling = true; }); submitButton.setEnabled(false); endCallButton = view.findViewById(R.id.rtt_end_call_button); endCallButton.setOnClickListener( v -> { LogUtil.i("RttChatFragment.onClick", "end call button clicked"); inCallButtonUiDelegate.onEndCallClicked(); }); overflowMenu = new RttOverflowMenu(getContext(), inCallButtonUiDelegate, inCallScreenDelegate); view.findViewById(R.id.rtt_overflow_button) .setOnClickListener( v -> { // Hide keyboard when opening overflow menu. This is alternative solution since hiding // keyboard after the menu is open or dialpad is shown doesn't work. UiUtil.hideKeyboardFrom(getContext(), editText); overflowMenu.showAtLocation(v, Gravity.TOP | Gravity.RIGHT, 0, 0); }); nameTextView = view.findViewById(R.id.rtt_name_or_number); chronometer = view.findViewById(R.id.rtt_timer); statusBanner = view.findViewById(R.id.rtt_status_banner); return view; } @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEND) { if (!TextUtils.isEmpty(editText.getText())) { Logger.get(getContext()) .logImpression(DialerImpression.Type.RTT_KEYBOARD_SEND_BUTTON_CLICKED); submitButton.performClick(); } return true; } return false; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (isClearingInput) { return; } String messageToAppend = adapter.computeChangeOfLocalMessage(s.toString()); if (!TextUtils.isEmpty(messageToAppend)) { adapter.addLocalMessage(messageToAppend); rttCallScreenDelegate.onLocalMessage(messageToAppend); } } @Override public void onRemoteMessage(String message) { adapter.addRemoteMessage(message); } @Override public void onDestroyView() { super.onDestroyView(); LogUtil.enterBlock("RttChatFragment.onDestroyView"); inCallButtonUiDelegate.onInCallButtonUiUnready(); inCallScreenDelegate.onInCallScreenUnready(); } @Override public void afterTextChanged(Editable s) { if (TextUtils.isEmpty(s)) { submitButton.setEnabled(false); } else { submitButton.setEnabled(true); } } @Override public void onUpdateLocalMessage(int position) { if (position < 0) { return; } recyclerView.smoothScrollToPosition(position); } @Override public void onUpdateRemoteMessage(int position) { if (position < 0) { return; } if (shouldAutoScrolling) { recyclerView.smoothScrollToPosition(position); } } @Override public void onRestoreRttChat(RttTranscript rttTranscript) { String unfinishedLocalMessage = adapter.onRestoreRttChat(rttTranscript); if (unfinishedLocalMessage != null) { resumeInput(unfinishedLocalMessage); } } private void resumeInput(String input) { isClearingInput = true; editText.setText(input); editText.setSelection(input.length()); isClearingInput = false; } @Override public void onStart() { LogUtil.enterBlock("RttChatFragment.onStart"); super.onStart(); isClearingInput = false; onRttScreenStart(); } @Override public void onStop() { LogUtil.enterBlock("RttChatFragment.onStop"); super.onStop(); isClearingInput = true; if (overflowMenu.isShowing()) { overflowMenu.dismiss(); } onRttScreenStop(); } @Override public void onRttScreenStart() { rttCallScreenDelegate.onRttCallScreenUiReady(); Activity activity = getActivity(); Window window = getActivity().getWindow(); window.setStatusBarColor(activity.getColor(R.color.rtt_status_bar_color)); window.setNavigationBarColor(activity.getColor(R.color.rtt_navigation_bar_color)); } @Override public void onRttScreenStop() { Activity activity = getActivity(); Window window = getActivity().getWindow(); window.setStatusBarColor(activity.getColor(android.R.color.transparent)); window.setNavigationBarColor(activity.getColor(android.R.color.transparent)); rttCallScreenDelegate.onRttCallScreenUiUnready(); } @Override public Fragment getRttCallScreenFragment() { return this; } @Override public String getCallId() { return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); } @Override public void setPrimary(@NonNull PrimaryInfo primaryInfo) { LogUtil.i("RttChatFragment.setPrimary", primaryInfo.toString()); nameTextView.setText(primaryInfo.name()); updateAvatar(primaryInfo); this.primaryInfo = primaryInfo; } private void updateAvatar(PrimaryInfo primaryInfo) { boolean hasPhoto = primaryInfo.photo() != null && primaryInfo.photoType() == ContactPhotoType.CONTACT; // Contact has a photo, don't render a letter tile. if (hasPhoto) { int avatarSize = getResources().getDimensionPixelSize(R.dimen.rtt_avatar_size); adapter.setAvatarDrawable( DrawableConverter.getRoundedDrawable( getContext(), primaryInfo.photo(), avatarSize, avatarSize)); } else { LetterTileDrawable letterTile = new LetterTileDrawable(getResources()); letterTile.setCanonicalDialerLetterTileDetails( primaryInfo.name(), primaryInfo.contactInfoLookupKey(), LetterTileDrawable.SHAPE_CIRCLE, LetterTileDrawable.getContactTypeFromPrimitives( primaryCallState.isVoiceMailNumber(), primaryInfo.isSpam(), primaryCallState.isBusinessNumber(), primaryInfo.numberPresentation(), primaryCallState.isConference())); adapter.setAvatarDrawable(letterTile); } } @Override public void onAttach(Context context) { super.onAttach(context); if (savedSecondaryInfo != null) { setSecondary(savedSecondaryInfo); } } @Override public void setSecondary(@NonNull SecondaryInfo secondaryInfo) { LogUtil.i("RttChatFragment.setSecondary", secondaryInfo.toString()); if (!isAdded()) { savedSecondaryInfo = secondaryInfo; return; } savedSecondaryInfo = null; FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.rtt_on_hold_banner); if (secondaryInfo.shouldShow()) { OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo); onHoldFragment.setPadTopInset(false); transaction.replace(R.id.rtt_on_hold_banner, onHoldFragment); } else { if (oldBanner != null) { transaction.remove(oldBanner); } } transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top); transaction.commitNowAllowingStateLoss(); overflowMenu.enableSwitchToSecondaryButton(secondaryInfo.shouldShow()); } @Override public void setCallState(@NonNull PrimaryCallState primaryCallState) { LogUtil.i("RttChatFragment.setCallState", primaryCallState.toString()); this.primaryCallState = primaryCallState; if (!isTimerStarted && primaryCallState.state() == DialerCallState.ACTIVE) { LogUtil.i( "RttChatFragment.setCallState", "starting timer with base: %d", chronometer.getBase()); chronometer.setBase( primaryCallState.connectTimeMillis() - System.currentTimeMillis() + SystemClock.elapsedRealtime()); chronometer.start(); isTimerStarted = true; editText.setVisibility(View.VISIBLE); submitButton.setVisibility(View.VISIBLE); editText.setFocusableInTouchMode(true); if (editText.requestFocus()) { UiUtil.showKeyboardFrom(getContext(), editText); } adapter.showAdvisory(); } if (primaryCallState.state() == DialerCallState.DIALING) { showWaitingForJoinBanner(); } else { hideWaitingForJoinBanner(); } if (primaryCallState.state() == DialerCallState.DISCONNECTED) { rttCallScreenDelegate.onSaveRttTranscript(); } } private void showWaitingForJoinBanner() { statusBanner.setText(getString(R.string.rtt_status_banner_text, primaryInfo.name())); statusBanner.setVisibility(View.VISIBLE); } private void hideWaitingForJoinBanner() { statusBanner.setVisibility(View.GONE); } @Override public void setEndCallButtonEnabled(boolean enabled, boolean animate) {} @Override public void showManageConferenceCallButton(boolean visible) {} @Override public boolean isManageConferenceVisible() { return false; } @Override public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {} @Override public void showNoteSentToast() {} @Override public void updateInCallScreenColors() {} @Override public void onInCallScreenDialpadVisibilityChange(boolean isShowing) { overflowMenu.setDialpadButtonChecked(isShowing); } @Override public int getAnswerAndDialpadContainerResourceId() { return R.id.incall_dialpad_container; } @Override public void showLocationUi(Fragment locationUi) {} @Override public boolean isShowingLocationUi() { return false; } @Override public Fragment getInCallScreenFragment() { return this; } @Override public void showButton(int buttonId, boolean show) { if (buttonId == InCallButtonIds.BUTTON_SWAP) { overflowMenu.enableSwapCallButton(show); } } @Override public void enableButton(int buttonId, boolean enable) {} @Override public void setEnabled(boolean on) {} @Override public void setHold(boolean on) {} @Override public void setCameraSwitched(boolean isBackFacingCamera) {} @Override public void setVideoPaused(boolean isPaused) {} @Override public void setAudioState(CallAudioState audioState) { LogUtil.i("RttChatFragment.setAudioState", "audioState: " + audioState); overflowMenu.setMuteButtonChecked(audioState.isMuted()); overflowMenu.setAudioState(audioState); if (audioSelectMenu != null) { audioSelectMenu.setAudioState(audioState); } } @Override public void updateButtonStates() {} @Override public void updateInCallButtonUiColors(int color) {} @Override public Fragment getInCallButtonUiFragment() { return this; } @Override public void showAudioRouteSelector() { audioSelectMenu = new AudioSelectMenu( getContext(), inCallButtonUiDelegate, () -> overflowMenu.showAtLocation(getView(), Gravity.TOP | Gravity.RIGHT, 0, 0)); audioSelectMenu.showAtLocation(getView(), Gravity.TOP | Gravity.RIGHT, 0, 0); } @Override public void onAudioRouteSelected(int audioRoute) { inCallButtonUiDelegate.setAudioRoute(audioRoute); } @Override public void onAudioRouteSelectorDismiss() {} }