1 /* 2 * Copyright (C) 2018 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.rtt.impl; 18 19 import android.app.Activity; 20 import android.os.Bundle; 21 import android.os.SystemClock; 22 import android.support.annotation.NonNull; 23 import android.support.annotation.Nullable; 24 import android.support.v4.app.Fragment; 25 import android.support.v7.widget.LinearLayoutManager; 26 import android.support.v7.widget.RecyclerView; 27 import android.support.v7.widget.RecyclerView.OnScrollListener; 28 import android.telecom.CallAudioState; 29 import android.text.Editable; 30 import android.text.TextUtils; 31 import android.text.TextWatcher; 32 import android.view.Gravity; 33 import android.view.KeyEvent; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.Window; 38 import android.view.accessibility.AccessibilityEvent; 39 import android.view.inputmethod.EditorInfo; 40 import android.view.inputmethod.InputMethodManager; 41 import android.widget.Chronometer; 42 import android.widget.EditText; 43 import android.widget.ImageButton; 44 import android.widget.TextView; 45 import android.widget.TextView.OnEditorActionListener; 46 import com.android.dialer.common.Assert; 47 import com.android.dialer.common.FragmentUtils; 48 import com.android.dialer.common.LogUtil; 49 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment; 50 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; 51 import com.android.incallui.call.DialerCall.State; 52 import com.android.incallui.incall.protocol.InCallButtonUi; 53 import com.android.incallui.incall.protocol.InCallButtonUiDelegate; 54 import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory; 55 import com.android.incallui.incall.protocol.InCallScreen; 56 import com.android.incallui.incall.protocol.InCallScreenDelegate; 57 import com.android.incallui.incall.protocol.InCallScreenDelegateFactory; 58 import com.android.incallui.incall.protocol.PrimaryCallState; 59 import com.android.incallui.incall.protocol.PrimaryInfo; 60 import com.android.incallui.incall.protocol.SecondaryInfo; 61 import com.android.incallui.rtt.impl.RttChatAdapter.MessageListener; 62 import com.android.incallui.rtt.protocol.Constants; 63 import com.android.incallui.rtt.protocol.RttCallScreen; 64 import com.android.incallui.rtt.protocol.RttCallScreenDelegate; 65 import com.android.incallui.rtt.protocol.RttCallScreenDelegateFactory; 66 67 /** RTT chat fragment to show chat bubbles. */ 68 public class RttChatFragment extends Fragment 69 implements OnEditorActionListener, 70 TextWatcher, 71 MessageListener, 72 RttCallScreen, 73 InCallScreen, 74 InCallButtonUi, 75 AudioRouteSelectorPresenter { 76 77 private static final String ARG_CALL_ID = "call_id"; 78 79 private RecyclerView recyclerView; 80 private RttChatAdapter adapter; 81 private EditText editText; 82 private ImageButton submitButton; 83 private boolean isClearingInput; 84 85 private final OnScrollListener onScrollListener = 86 new OnScrollListener() { 87 @Override 88 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 89 if (dy < 0) { 90 hideKeyboard(); 91 } 92 } 93 }; 94 private InCallScreenDelegate inCallScreenDelegate; 95 private RttCallScreenDelegate rttCallScreenDelegate; 96 private InCallButtonUiDelegate inCallButtonUiDelegate; 97 private View endCallButton; 98 private TextView nameTextView; 99 private Chronometer chronometer; 100 private boolean isTimerStarted; 101 private RttOverflowMenu overflowMenu; 102 103 /** 104 * Create a new instance of RttChatFragment. 105 * 106 * @param callId call id of the RTT call. 107 * @return new RttChatFragment 108 */ 109 public static RttChatFragment newInstance(String callId) { 110 Bundle bundle = new Bundle(); 111 bundle.putString(ARG_CALL_ID, callId); 112 RttChatFragment instance = new RttChatFragment(); 113 instance.setArguments(bundle); 114 return instance; 115 } 116 117 @Override 118 public void onCreate(@Nullable Bundle savedInstanceState) { 119 super.onCreate(savedInstanceState); 120 LogUtil.i("RttChatFragment.onCreate", null); 121 inCallButtonUiDelegate = 122 FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class) 123 .newInCallButtonUiDelegate(); 124 if (savedInstanceState != null) { 125 inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState); 126 } 127 } 128 129 @Override 130 public void onViewCreated(@NonNull View view, @Nullable Bundle bundle) { 131 super.onViewCreated(view, bundle); 132 LogUtil.i("RttChatFragment.onViewCreated", null); 133 134 inCallScreenDelegate = 135 FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class) 136 .newInCallScreenDelegate(); 137 rttCallScreenDelegate = 138 FragmentUtils.getParentUnsafe(this, RttCallScreenDelegateFactory.class) 139 .newRttCallScreenDelegate(this); 140 141 rttCallScreenDelegate.initRttCallScreenDelegate(this); 142 143 inCallScreenDelegate.onInCallScreenDelegateInit(this); 144 inCallScreenDelegate.onInCallScreenReady(); 145 inCallButtonUiDelegate.onInCallButtonUiReady(this); 146 } 147 148 @Nullable 149 @Override 150 public View onCreateView( 151 LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { 152 View view = inflater.inflate(R.layout.frag_rtt_chat, container, false); 153 editText = view.findViewById(R.id.rtt_chat_input); 154 editText.setOnEditorActionListener(this); 155 editText.addTextChangedListener(this); 156 recyclerView = view.findViewById(R.id.rtt_recycler_view); 157 LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); 158 layoutManager.setStackFromEnd(true); 159 recyclerView.setLayoutManager(layoutManager); 160 recyclerView.setHasFixedSize(false); 161 adapter = new RttChatAdapter(getContext(), this); 162 recyclerView.setAdapter(adapter); 163 recyclerView.addOnScrollListener(onScrollListener); 164 submitButton = view.findViewById(R.id.rtt_chat_submit_button); 165 submitButton.setOnClickListener( 166 v -> { 167 adapter.submitLocalMessage(); 168 isClearingInput = true; 169 editText.setText(""); 170 isClearingInput = false; 171 rttCallScreenDelegate.onLocalMessage(Constants.BUBBLE_BREAKER); 172 }); 173 submitButton.setEnabled(false); 174 endCallButton = view.findViewById(R.id.rtt_end_call_button); 175 endCallButton.setOnClickListener( 176 v -> { 177 LogUtil.i("RttChatFragment.onClick", "end call button clicked"); 178 inCallButtonUiDelegate.onEndCallClicked(); 179 }); 180 181 overflowMenu = new RttOverflowMenu(getContext(), inCallButtonUiDelegate); 182 view.findViewById(R.id.rtt_overflow_button) 183 .setOnClickListener(v -> overflowMenu.showAtLocation(v, Gravity.TOP | Gravity.RIGHT, 0, 0)); 184 185 nameTextView = view.findViewById(R.id.rtt_name_or_number); 186 chronometer = view.findViewById(R.id.rtt_timer); 187 return view; 188 } 189 190 @Override 191 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 192 if (actionId == EditorInfo.IME_ACTION_DONE) { 193 submitButton.performClick(); 194 return true; 195 } 196 return false; 197 } 198 199 @Override 200 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 201 202 @Override 203 public void onTextChanged(CharSequence s, int start, int before, int count) { 204 if (isClearingInput) { 205 return; 206 } 207 String messageToAppend = RttChatMessage.getChangedString(s, start, before, count); 208 if (!TextUtils.isEmpty(messageToAppend)) { 209 adapter.addLocalMessage(messageToAppend); 210 rttCallScreenDelegate.onLocalMessage(messageToAppend); 211 } 212 } 213 214 @Override 215 public void onRemoteMessage(String message) { 216 adapter.addRemoteMessage(message); 217 } 218 219 @Override 220 public void onDestroyView() { 221 super.onDestroyView(); 222 LogUtil.enterBlock("RttChatFragment.onDestroyView"); 223 inCallButtonUiDelegate.onInCallButtonUiUnready(); 224 inCallScreenDelegate.onInCallScreenUnready(); 225 } 226 227 @Override 228 public void afterTextChanged(Editable s) { 229 if (TextUtils.isEmpty(s)) { 230 submitButton.setEnabled(false); 231 } else { 232 submitButton.setEnabled(true); 233 } 234 } 235 236 @Override 237 public void newMessageAdded() { 238 recyclerView.smoothScrollToPosition(adapter.getItemCount()); 239 } 240 241 @Override 242 public void onStart() { 243 LogUtil.enterBlock("RttChatFragment.onStart"); 244 super.onStart(); 245 onRttScreenStart(); 246 } 247 248 @Override 249 public void onStop() { 250 LogUtil.enterBlock("RttChatFragment.onStop"); 251 super.onStop(); 252 if (overflowMenu.isShowing()) { 253 overflowMenu.dismiss(); 254 } 255 onRttScreenStop(); 256 } 257 258 private void hideKeyboard() { 259 InputMethodManager inputMethodManager = getContext().getSystemService(InputMethodManager.class); 260 if (inputMethodManager.isAcceptingText()) { 261 inputMethodManager.hideSoftInputFromWindow( 262 getActivity().getCurrentFocus().getWindowToken(), 0); 263 } 264 } 265 266 @Override 267 public void onRttScreenStart() { 268 rttCallScreenDelegate.onRttCallScreenUiReady(); 269 Activity activity = getActivity(); 270 Window window = getActivity().getWindow(); 271 window.setStatusBarColor(activity.getColor(R.color.rtt_status_bar_color)); 272 window.setNavigationBarColor(activity.getColor(R.color.rtt_navigation_bar_color)); 273 window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); 274 } 275 276 @Override 277 public void onRttScreenStop() { 278 rttCallScreenDelegate.onRttCallScreenUiUnready(); 279 } 280 281 @Override 282 public Fragment getRttCallScreenFragment() { 283 return this; 284 } 285 286 @Override 287 public String getCallId() { 288 return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); 289 } 290 291 @Override 292 public void setPrimary(@NonNull PrimaryInfo primaryInfo) { 293 LogUtil.i("RttChatFragment.setPrimary", primaryInfo.toString()); 294 nameTextView.setText(primaryInfo.name()); 295 } 296 297 @Override 298 public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {} 299 300 @Override 301 public void setCallState(@NonNull PrimaryCallState primaryCallState) { 302 LogUtil.i("RttChatFragment.setCallState", primaryCallState.toString()); 303 if (!isTimerStarted && primaryCallState.state() == State.ACTIVE) { 304 LogUtil.i( 305 "RttChatFragment.setCallState", "starting timer with base: %d", chronometer.getBase()); 306 chronometer.setBase( 307 primaryCallState.connectTimeMillis() 308 - System.currentTimeMillis() 309 + SystemClock.elapsedRealtime()); 310 chronometer.start(); 311 isTimerStarted = true; 312 } 313 } 314 315 @Override 316 public void setEndCallButtonEnabled(boolean enabled, boolean animate) {} 317 318 @Override 319 public void showManageConferenceCallButton(boolean visible) {} 320 321 @Override 322 public boolean isManageConferenceVisible() { 323 return false; 324 } 325 326 @Override 327 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {} 328 329 @Override 330 public void showNoteSentToast() {} 331 332 @Override 333 public void updateInCallScreenColors() {} 334 335 @Override 336 public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {} 337 338 @Override 339 public int getAnswerAndDialpadContainerResourceId() { 340 return 0; 341 } 342 343 @Override 344 public void showLocationUi(Fragment locationUi) {} 345 346 @Override 347 public boolean isShowingLocationUi() { 348 return false; 349 } 350 351 @Override 352 public Fragment getInCallScreenFragment() { 353 return this; 354 } 355 356 @Override 357 public void showButton(int buttonId, boolean show) {} 358 359 @Override 360 public void enableButton(int buttonId, boolean enable) {} 361 362 @Override 363 public void setEnabled(boolean on) {} 364 365 @Override 366 public void setHold(boolean on) {} 367 368 @Override 369 public void setCameraSwitched(boolean isBackFacingCamera) {} 370 371 @Override 372 public void setVideoPaused(boolean isPaused) {} 373 374 @Override 375 public void setAudioState(CallAudioState audioState) { 376 LogUtil.i("RttChatFragment.setAudioState", "audioState: " + audioState); 377 overflowMenu.setMuteButtonChecked(audioState.isMuted()); 378 overflowMenu.setAudioState(audioState); 379 } 380 381 @Override 382 public void updateButtonStates() {} 383 384 @Override 385 public void updateInCallButtonUiColors(int color) {} 386 387 @Override 388 public Fragment getInCallButtonUiFragment() { 389 return this; 390 } 391 392 @Override 393 public void showAudioRouteSelector() { 394 AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState()) 395 .show(getChildFragmentManager(), null); 396 } 397 398 @Override 399 public void onAudioRouteSelected(int audioRoute) { 400 inCallButtonUiDelegate.setAudioRoute(audioRoute); 401 } 402 403 @Override 404 public void onAudioRouteSelectorDismiss() {} 405 } 406