1 /* 2 * Copyright (C) 2015 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 package com.android.car.dialer; 17 18 import static com.android.car.dialer.ui.CallHistoryFragment.CALL_TYPE_KEY; 19 20 import android.content.Intent; 21 import android.graphics.PorterDuff; 22 import android.graphics.drawable.Drawable; 23 import android.os.Bundle; 24 import android.support.annotation.Nullable; 25 import android.support.annotation.StringRes; 26 import android.support.v4.app.Fragment; 27 import android.telecom.Call; 28 import android.telephony.PhoneNumberUtils; 29 import android.util.Log; 30 31 import androidx.car.drawer.CarDrawerActivity; 32 import androidx.car.drawer.CarDrawerAdapter; 33 import androidx.car.drawer.DrawerItemViewHolder; 34 35 import com.android.car.dialer.telecom.InMemoryPhoneBook; 36 import com.android.car.dialer.telecom.PhoneLoader; 37 import com.android.car.dialer.telecom.UiCall; 38 import com.android.car.dialer.telecom.UiCallManager; 39 import com.android.car.dialer.ui.CallHistoryFragment; 40 import com.android.car.dialer.ui.ContactListFragment; 41 import com.android.car.dialer.ui.InCallFragment; 42 43 import java.util.stream.Stream; 44 45 /** 46 * Main activity for the Dialer app. Displays different fragments depending on call and 47 * connectivity status: 48 * <ul> 49 * <li>OngoingCallFragment 50 * <li>NoHfpFragment 51 * <li>DialerFragment 52 * <li>StrequentFragment 53 * </ul> 54 */ 55 public class TelecomActivity extends CarDrawerActivity implements CallListener { 56 private static final String TAG = "TelecomActivity"; 57 58 private static final String ACTION_ANSWER_CALL = "com.android.car.dialer.ANSWER_CALL"; 59 private static final String ACTION_END_CALL = "com.android.car.dialer.END_CALL"; 60 61 private static final String DIALER_BACKSTACK = "DialerBackstack"; 62 private static final String CONTENT_FRAGMENT_TAG = "CONTENT_FRAGMENT_TAG"; 63 private static final String DIALER_FRAGMENT_TAG = "DIALER_FRAGMENT_TAG"; 64 65 private final UiBluetoothMonitor.Listener mBluetoothListener = this::updateCurrentFragment; 66 67 private UiCallManager mUiCallManager; 68 private UiBluetoothMonitor mUiBluetoothMonitor; 69 70 /** 71 * Whether or not it is safe to make transactions on the 72 * {@link android.support.v4.app.FragmentManager}. This variable prevents a possible exception 73 * when calling commit() on the FragmentManager. 74 * 75 * <p>The default value is {@code true} because it is only after 76 * {@link #onSaveInstanceState(Bundle)} that fragment commits are not allowed. 77 */ 78 private boolean mAllowFragmentCommits = true; 79 80 @Override onCreate(Bundle savedInstanceState)81 protected void onCreate(Bundle savedInstanceState) { 82 super.onCreate(savedInstanceState); 83 setToolbarElevation(0f); 84 85 if (vdebug()) { 86 Log.d(TAG, "onCreate"); 87 } 88 89 setMainContent(R.layout.telecom_activity); 90 getWindow().getDecorView().setBackgroundColor(getColor(R.color.phone_theme)); 91 updateTitle(); 92 93 mUiCallManager = UiCallManager.init(getApplicationContext()); 94 mUiBluetoothMonitor = new UiBluetoothMonitor(this); 95 96 InMemoryPhoneBook.init(getApplicationContext()); 97 98 findViewById(R.id.search).setOnClickListener( 99 v -> startActivity(new Intent(this, ContactSearchActivity.class))); 100 101 getDrawerController().setRootAdapter(new DialerRootAdapter()); 102 } 103 104 @Override onDestroy()105 protected void onDestroy() { 106 super.onDestroy(); 107 if (vdebug()) { 108 Log.d(TAG, "onDestroy"); 109 } 110 mUiBluetoothMonitor.tearDown(); 111 InMemoryPhoneBook.tearDown(); 112 mUiCallManager.tearDown(); 113 mUiCallManager = null; 114 } 115 116 @Override onStop()117 protected void onStop() { 118 super.onStop(); 119 mUiCallManager.removeListener(this); 120 mUiBluetoothMonitor.removeListener(mBluetoothListener); 121 } 122 123 @Override onSaveInstanceState(Bundle outState)124 public void onSaveInstanceState(Bundle outState) { 125 // A transaction can only be committed with this method prior to its containing activity 126 // saving its state. 127 mAllowFragmentCommits = false; 128 super.onSaveInstanceState(outState); 129 } 130 131 @Override onNewIntent(Intent i)132 protected void onNewIntent(Intent i) { 133 super.onNewIntent(i); 134 setIntent(i); 135 } 136 137 @Override onStart()138 protected void onStart() { 139 if (vdebug()) { 140 Log.d(TAG, "onStart"); 141 } 142 super.onStart(); 143 144 // Fragment commits are not allowed once the Activity's state has been saved. Once 145 // onStart() has been called, the FragmentManager should now allow commits. 146 mAllowFragmentCommits = true; 147 148 // Update the current fragment before handling the intent so that any UI updates in 149 // handleIntent() is not overridden by updateCurrentFragment(). 150 updateCurrentFragment(); 151 handleIntent(); 152 153 mUiCallManager.addListener(this); 154 mUiBluetoothMonitor.addListener(mBluetoothListener); 155 } 156 handleIntent()157 private void handleIntent() { 158 Intent intent = getIntent(); 159 String action = intent != null ? intent.getAction() : null; 160 161 if (vdebug()) { 162 Log.d(TAG, "handleIntent, intent: " + intent + ", action: " + action); 163 } 164 165 if (action == null || action.length() == 0) { 166 return; 167 } 168 169 String number; 170 UiCall ringingCall; 171 switch (action) { 172 case ACTION_ANSWER_CALL: 173 ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING); 174 if (ringingCall == null) { 175 Log.e(TAG, "Unable to answer ringing call. There is none."); 176 } else { 177 mUiCallManager.answerCall(ringingCall); 178 } 179 break; 180 181 case ACTION_END_CALL: 182 ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING); 183 if (ringingCall == null) { 184 Log.e(TAG, "Unable to end ringing call. There is none."); 185 } else { 186 mUiCallManager.disconnectCall(ringingCall); 187 } 188 break; 189 190 case Intent.ACTION_DIAL: 191 number = PhoneNumberUtils.getNumberFromIntent(intent, this); 192 if (!(getCurrentFragment() instanceof NoHfpFragment)) { 193 showDialer(number); 194 } 195 break; 196 197 case Intent.ACTION_CALL: 198 number = PhoneNumberUtils.getNumberFromIntent(intent, this); 199 mUiCallManager.safePlaceCall(number, false /* bluetoothRequired */); 200 break; 201 202 default: 203 // Do nothing. 204 } 205 206 setIntent(null); 207 } 208 209 /** 210 * Updates the content fragment of this Activity based on the state of the application. 211 */ updateCurrentFragment()212 private void updateCurrentFragment() { 213 if (vdebug()) { 214 Log.d(TAG, "updateCurrentFragment()"); 215 } 216 217 boolean callEmpty = mUiCallManager.getCalls().isEmpty(); 218 if (!mUiBluetoothMonitor.isBluetoothEnabled() && callEmpty) { 219 showNoHfpFragment(R.string.bluetooth_disabled); 220 } else if (!mUiBluetoothMonitor.isBluetoothPaired() && callEmpty) { 221 showNoHfpFragment(R.string.bluetooth_unpaired); 222 } else if (!mUiBluetoothMonitor.isHfpConnected() && callEmpty) { 223 showNoHfpFragment(R.string.no_hfp); 224 } else { 225 UiCall ongoingCall = mUiCallManager.getPrimaryCall(); 226 227 if (vdebug()) { 228 Log.d(TAG, "ongoingCall: " + ongoingCall + ", mCurrentFragment: " 229 + getCurrentFragment()); 230 } 231 232 if (ongoingCall == null && getCurrentFragment() instanceof InCallFragment) { 233 showSpeedDialFragment(); 234 } else if (ongoingCall != null) { 235 showOngoingCallFragment(); 236 } else { 237 showSpeedDialFragment(); 238 } 239 } 240 241 if (vdebug()) { 242 Log.d(TAG, "updateCurrentFragment: done"); 243 } 244 } 245 showSpeedDialFragment()246 private void showSpeedDialFragment() { 247 if (vdebug()) { 248 Log.d(TAG, "showSpeedDialFragment"); 249 } 250 251 if (!mAllowFragmentCommits || getCurrentFragment() instanceof StrequentsFragment) { 252 return; 253 } 254 255 Fragment fragment = StrequentsFragment.newInstance(); 256 setContentFragment(fragment); 257 } 258 showOngoingCallFragment()259 private void showOngoingCallFragment() { 260 if (vdebug()) { 261 Log.d(TAG, "showOngoingCallFragment"); 262 } 263 if (!mAllowFragmentCommits || getCurrentFragment() instanceof InCallFragment) { 264 // in case the dialer is still open, (e.g. when dialing the second phone during 265 // a phone call), close it 266 maybeHideDialer(); 267 getDrawerController().closeDrawer(); 268 return; 269 } 270 Fragment fragment = InCallFragment.newInstance(); 271 setContentFragmentWithFadeAnimation(fragment); 272 getDrawerController().closeDrawer(); 273 } 274 showDialer()275 private void showDialer() { 276 if (vdebug()) { 277 Log.d(TAG, "showDialer"); 278 } 279 280 showDialer(null /* dialNumber */); 281 } 282 283 /** 284 * Displays the {@link DialerFragment} and initialize it with the given phone number. 285 */ showDialer(@ullable String dialNumber)286 private void showDialer(@Nullable String dialNumber) { 287 if (vdebug()) { 288 Log.d(TAG, "showDialer with number: " + dialNumber); 289 } 290 291 if (!mAllowFragmentCommits || 292 getSupportFragmentManager().findFragmentByTag(DIALER_FRAGMENT_TAG) != null) { 293 return; 294 } 295 296 Fragment fragment = DialerFragment.newInstance(dialNumber); 297 // Add the dialer fragment to the backstack so that it can be popped off to dismiss it. 298 setContentFragment(fragment); 299 } 300 301 /** 302 * Checks if the dialpad fragment is opened and hides it if it is. 303 */ maybeHideDialer()304 private void maybeHideDialer() { 305 // The dialer is the only fragment to be added to the back stack. Dismiss the dialer by 306 // removing it from the back stack. 307 if (getSupportFragmentManager().getBackStackEntryCount() > 0) { 308 getSupportFragmentManager().popBackStack(); 309 } 310 } 311 showNoHfpFragment(@tringRes int stringResId)312 private void showNoHfpFragment(@StringRes int stringResId) { 313 if (!mAllowFragmentCommits) { 314 return; 315 } 316 317 String errorMessage = getString(stringResId); 318 Fragment currentFragment = getCurrentFragment(); 319 320 if (currentFragment instanceof NoHfpFragment) { 321 ((NoHfpFragment) currentFragment).setErrorMessage(errorMessage); 322 } else { 323 setContentFragment(NoHfpFragment.newInstance(errorMessage)); 324 } 325 } 326 setContentFragmentWithSlideAndDelayAnimation(Fragment fragment)327 private void setContentFragmentWithSlideAndDelayAnimation(Fragment fragment) { 328 if (vdebug()) { 329 Log.d(TAG, "setContentFragmentWithSlideAndDelayAnimation, fragment: " + fragment); 330 } 331 setContentFragmentWithAnimations(fragment, 332 R.anim.telecom_slide_in_with_delay, R.anim.telecom_slide_out); 333 } 334 setContentFragmentWithFadeAnimation(Fragment fragment)335 private void setContentFragmentWithFadeAnimation(Fragment fragment) { 336 if (vdebug()) { 337 Log.d(TAG, "setContentFragmentWithFadeAnimation, fragment: " + fragment); 338 } 339 setContentFragmentWithAnimations(fragment, 340 R.anim.telecom_fade_in, R.anim.telecom_fade_out); 341 } 342 setContentFragmentWithAnimations(Fragment fragment, int enter, int exit)343 private void setContentFragmentWithAnimations(Fragment fragment, int enter, int exit) { 344 if (vdebug()) { 345 Log.d(TAG, "setContentFragmentWithAnimations: " + fragment); 346 } 347 348 maybeHideDialer(); 349 350 getSupportFragmentManager().beginTransaction() 351 .setCustomAnimations(enter, exit) 352 .replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG) 353 .commitNow(); 354 } 355 356 /** 357 * Sets the fragment that will be shown as the main content of this Activity. Note that this 358 * fragment is not always visible. In particular, the dialer fragment can show up on top of this 359 * fragment. 360 */ setContentFragment(Fragment fragment)361 private void setContentFragment(Fragment fragment) { 362 maybeHideDialer(); 363 getSupportFragmentManager().beginTransaction() 364 .replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG) 365 .commitNow(); 366 updateTitle(); 367 } 368 369 /** 370 * Returns the fragment that is currently being displayed as the content view. Note that this 371 * is not necessarily the fragment that is visible. For example, the returned fragment 372 * could be the content, but the dial fragment is being displayed on top of it. Check for 373 * the existence of the dial fragment with the TAG {@link #DIALER_FRAGMENT_TAG}. 374 */ 375 @Nullable getCurrentFragment()376 private Fragment getCurrentFragment() { 377 return getSupportFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG); 378 } 379 vdebug()380 private static boolean vdebug() { 381 return Log.isLoggable(TAG, Log.DEBUG); 382 } 383 384 @Override onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask)385 public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) { 386 fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment) 387 .onAudioStateChanged(isMuted, route, supportedRouteMask)); 388 } 389 390 @Override onCallStateChanged(UiCall call, int state)391 public void onCallStateChanged(UiCall call, int state) { 392 if (vdebug()) { 393 Log.d(TAG, "onCallStateChanged"); 394 } 395 updateCurrentFragment(); 396 397 fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment) 398 .onCallStateChanged(call, state)); 399 } 400 401 @Override onCallUpdated(UiCall call)402 public void onCallUpdated(UiCall call) { 403 if (vdebug()) { 404 Log.d(TAG, "onCallUpdated"); 405 } 406 updateCurrentFragment(); 407 408 fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment) 409 .onCallUpdated(call)); 410 } 411 412 @Override onCallAdded(UiCall call)413 public void onCallAdded(UiCall call) { 414 if (vdebug()) { 415 Log.d(TAG, "onCallAdded"); 416 } 417 updateCurrentFragment(); 418 419 fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment) 420 .onCallAdded(call)); 421 } 422 423 @Override onCallRemoved(UiCall call)424 public void onCallRemoved(UiCall call) { 425 if (vdebug()) { 426 Log.d(TAG, "onCallRemoved"); 427 } 428 updateCurrentFragment(); 429 430 fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment) 431 .onCallRemoved(call)); 432 } 433 shouldPropagateCallback(Fragment fragment)434 private static boolean shouldPropagateCallback(Fragment fragment) { 435 return fragment instanceof CallListener && fragment.isAdded(); 436 } 437 fragmentsToPropagateCallback()438 private Stream<Fragment> fragmentsToPropagateCallback() { 439 return getSupportFragmentManager().getFragments().stream() 440 .filter(fragment -> shouldPropagateCallback(fragment)); 441 } 442 443 private class DialerRootAdapter extends CarDrawerAdapter { 444 private static final int ITEM_FAVORITES = 0; 445 private static final int ITEM_CALLLOG_ALL = 1; 446 private static final int ITEM_CALLLOG_MISSED = 2; 447 private static final int ITEM_CONTACT = 3; 448 private static final int ITEM_DIAL = 4; 449 450 private static final int ITEM_COUNT = 5; 451 DialerRootAdapter()452 DialerRootAdapter() { 453 super(TelecomActivity.this, false /* showDisabledListOnEmpty */); 454 } 455 456 @Override getActualItemCount()457 protected int getActualItemCount() { 458 return ITEM_COUNT; 459 } 460 461 @Override populateViewHolder(DrawerItemViewHolder holder, int position)462 public void populateViewHolder(DrawerItemViewHolder holder, int position) { 463 final int iconColor = getResources().getColor(R.color.car_tint); 464 int textResId, iconResId; 465 switch (position) { 466 case ITEM_DIAL: 467 textResId = R.string.calllog_dial_number; 468 iconResId = R.drawable.ic_drawer_dialpad; 469 break; 470 case ITEM_CALLLOG_ALL: 471 textResId = R.string.calllog_all; 472 iconResId = R.drawable.ic_drawer_history; 473 break; 474 case ITEM_CALLLOG_MISSED: 475 textResId = R.string.calllog_missed; 476 iconResId = R.drawable.ic_call_missed; 477 break; 478 case ITEM_FAVORITES: 479 textResId = R.string.calllog_favorites; 480 iconResId = R.drawable.ic_favorite; 481 break; 482 case ITEM_CONTACT: 483 textResId = R.string.contact_menu_label; 484 iconResId = R.drawable.ic_contact; 485 break; 486 default: 487 Log.wtf(TAG, "Unexpected position: " + position); 488 return; 489 } 490 holder.getTitle().setText(textResId); 491 Drawable drawable = getDrawable(iconResId); 492 drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); 493 holder.getIcon().setImageDrawable(drawable); 494 } 495 496 @Override onItemClick(int position)497 public void onItemClick(int position) { 498 getDrawerController().closeDrawer(); 499 switch (position) { 500 case ITEM_DIAL: 501 showDialer(); 502 break; 503 case ITEM_CALLLOG_ALL: 504 showCallHistory(PhoneLoader.CallType.CALL_TYPE_ALL); 505 break; 506 case ITEM_CALLLOG_MISSED: 507 showCallHistory(PhoneLoader.CallType.MISSED_TYPE); 508 break; 509 case ITEM_FAVORITES: 510 showSpeedDialFragment(); 511 break; 512 case ITEM_CONTACT: 513 showContact(); 514 break; 515 default: 516 Log.w(TAG, "Invalid position in ROOT menu! " + position); 517 } 518 setTitle(getTitleString()); 519 } 520 } 521 showCallHistory(@honeLoader.CallType int callType)522 private void showCallHistory(@PhoneLoader.CallType int callType) { 523 setContentFragment(CallHistoryFragment.newInstance(callType)); 524 } 525 showContact()526 private void showContact() { 527 setContentFragment(ContactListFragment.newInstance()); 528 } 529 updateTitle()530 private void updateTitle() { 531 setTitle(getTitleString()); 532 } 533 getTitleString()534 private String getTitleString() { 535 Fragment currentFragment = getCurrentFragment(); 536 537 int titleResId = R.string.phone_app_name; 538 539 if (currentFragment instanceof StrequentsFragment) { 540 titleResId = R.string.contacts_title; 541 } else if (currentFragment instanceof CallHistoryFragment) { 542 int callType = currentFragment.getArguments().getInt(CALL_TYPE_KEY); 543 if (callType == PhoneLoader.CallType.MISSED_TYPE) { 544 titleResId = R.string.missed_call_title; 545 } else { 546 titleResId = R.string.call_history_title; 547 } 548 } else if (currentFragment instanceof ContactListFragment) { 549 titleResId = R.string.contacts_title; 550 } else if (currentFragment instanceof DialerFragment) { 551 titleResId = R.string.dialpad_title; 552 } else if (currentFragment instanceof InCallFragment 553 || currentFragment instanceof OngoingCallFragment) { 554 titleResId = R.string.in_call_title; 555 } 556 557 return getString(titleResId); 558 } 559 } 560