/* * 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.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; import com.android.incallui.call.DialerCall.State; import com.android.incallui.hold.OnHoldFragment; 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; /** 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 final OnScrollListener onScrollListener = new OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (dy < 0) { UiUtil.hideKeyboardFrom(getContext(), editText); } } }; 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; /** * 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); } @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 means the // current // input box is empty. if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) { String lastMessage = adapter.retrieveLastLocalMessage(); if (lastMessage != null) { isClearingInput = true; editText.setText(lastMessage); editText.setSelection(lastMessage.length()); isClearingInput = false; 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, savedInstanceState); recyclerView.setAdapter(adapter); recyclerView.addOnScrollListener(onScrollListener); submitButton = view.findViewById(R.id.rtt_chat_submit_button); submitButton.setOnClickListener( v -> { adapter.submitLocalMessage(); isClearingInput = true; editText.setText(""); isClearingInput = false; rttCallScreenDelegate.onLocalMessage(Constants.BUBBLE_BREAKER); }); 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); return view; } @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEND) { if (!TextUtils.isEmpty(editText.getText())) { 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 newMessageAdded() { recyclerView.smoothScrollToPosition(adapter.getItemCount()); } @Override public void onStart() { LogUtil.enterBlock("RttChatFragment.onStart"); super.onStart(); isClearingInput = false; onRttScreenStart(); } @Override public void onSaveInstanceState(@NonNull Bundle bundle) { super.onSaveInstanceState(bundle); adapter.onSaveInstanceState(bundle); } @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()); } @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()); if (!isTimerStarted && primaryCallState.state() == State.ACTIVE) { LogUtil.i( "RttChatFragment.setCallState", "starting timer with base: %d", chronometer.getBase()); chronometer.setBase( primaryCallState.connectTimeMillis() - System.currentTimeMillis() + SystemClock.elapsedRealtime()); chronometer.start(); isTimerStarted = true; } } @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); } @Override public void updateButtonStates() {} @Override public void updateInCallButtonUiColors(int color) {} @Override public Fragment getInCallButtonUiFragment() { return this; } @Override public void showAudioRouteSelector() { AudioSelectMenu 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() {} }