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 android.content.Intent; 19 import android.graphics.PorterDuff; 20 import android.graphics.drawable.Drawable; 21 import android.os.Bundle; 22 import android.support.v4.app.Fragment; 23 import android.telecom.Call; 24 import android.telephony.PhoneNumberUtils; 25 import android.util.Log; 26 27 import com.android.car.app.CarDrawerActivity; 28 import com.android.car.app.CarDrawerAdapter; 29 import com.android.car.app.DrawerItemViewHolder; 30 import com.android.car.dialer.bluetooth.UiBluetoothMonitor; 31 import com.android.car.dialer.telecom.PhoneLoader; 32 import com.android.car.dialer.telecom.UiCall; 33 import com.android.car.dialer.telecom.UiCallManager; 34 import com.android.car.dialer.telecom.UiCallManager.CallListener; 35 36 import java.util.List; 37 38 /** 39 * Main activity for the Dialer app. Displays different fragments depending on call and 40 * connectivity status: 41 * <ul> 42 * <li>OngoingCallFragment 43 * <li>NoHfpFragment 44 * <li>DialerFragment 45 * <li>StrequentFragment 46 * </ul> 47 */ 48 public class TelecomActivity extends CarDrawerActivity implements 49 DialerFragment.DialerBackButtonListener { 50 private static final String TAG = "Em.TelecomActivity"; 51 52 private static final String ACTION_ANSWER_CALL = "com.android.car.dialer.ANSWER_CALL"; 53 private static final String ACTION_END_CALL = "com.android.car.dialer.END_CALL"; 54 private static final String DIALER_BACKSTACK = "DialerBackstack"; 55 private static final String FRAGMENT_CLASS_KEY = "FRAGMENT_CLASS_KEY"; 56 57 private final UiBluetoothMonitor.Listener mBluetoothListener = this::updateCurrentFragment; 58 59 private UiCallManager mUiCallManager; 60 private UiBluetoothMonitor mUiBluetoothMonitor; 61 62 private Fragment mCurrentFragment; 63 private String mCurrentFragmentName; 64 65 private int mLastNoHfpMessageId; 66 private StrequentsFragment mSpeedDialFragment; 67 private Fragment mOngoingCallFragment; 68 69 private DialerFragment mDialerFragment; 70 private boolean mDialerFragmentOpened; 71 72 @Override onCreate(Bundle savedInstanceState)73 protected void onCreate(Bundle savedInstanceState) { 74 super.onCreate(savedInstanceState); 75 76 if (vdebug()) { 77 Log.d(TAG, "onCreate"); 78 } 79 getWindow().getDecorView().setBackgroundColor(getColor(R.color.phone_theme)); 80 setTitle(getString(R.string.phone_app_name)); 81 82 mUiCallManager = UiCallManager.getInstance(this); 83 mUiBluetoothMonitor = UiBluetoothMonitor.getInstance(); 84 85 if (savedInstanceState != null) { 86 mCurrentFragmentName = savedInstanceState.getString(FRAGMENT_CLASS_KEY); 87 } 88 89 if (vdebug()) { 90 Log.d(TAG, "onCreate done, mCurrentFragmentName: " + mCurrentFragmentName); 91 } 92 } 93 94 @Override onDestroy()95 protected void onDestroy() { 96 super.onDestroy(); 97 if (vdebug()) { 98 Log.d(TAG, "onDestroy"); 99 } 100 mUiCallManager = null; 101 } 102 103 @Override onPause()104 protected void onPause() { 105 super.onPause(); 106 mUiCallManager.removeListener(mCarCallListener); 107 mUiBluetoothMonitor.removeListener(mBluetoothListener); 108 } 109 110 @Override onSaveInstanceState(Bundle outState)111 public void onSaveInstanceState(Bundle outState) { 112 if (mCurrentFragment != null) { 113 outState.putString(FRAGMENT_CLASS_KEY, mCurrentFragmentName); 114 } 115 super.onSaveInstanceState(outState); 116 } 117 118 @Override onNewIntent(Intent i)119 protected void onNewIntent(Intent i) { 120 super.onNewIntent(i); 121 setIntent(i); 122 } 123 124 @Override onResume()125 protected void onResume() { 126 if (vdebug()) { 127 Log.d(TAG, "onResume"); 128 } 129 super.onResume(); 130 131 // Update the current fragment before handling the intent so that any UI updates in 132 // handleIntent() is not overridden by updateCurrentFragment(). 133 updateCurrentFragment(); 134 handleIntent(); 135 136 mUiCallManager.addListener(mCarCallListener); 137 mUiBluetoothMonitor.addListener(mBluetoothListener); 138 } 139 140 // TODO: move to base class. setContentFragmentWithAnimations(Fragment fragment, int enter, int exit)141 private void setContentFragmentWithAnimations(Fragment fragment, int enter, int exit) { 142 if (vdebug()) { 143 Log.d(TAG, "setContentFragmentWithAnimations: " + fragment); 144 } 145 146 maybeHideDialer(); 147 148 getSupportFragmentManager().beginTransaction() 149 .setCustomAnimations(enter, exit) 150 .replace(getContentContainerId(), fragment) 151 .commitAllowingStateLoss(); 152 153 mCurrentFragmentName = fragment.getClass().getSimpleName(); 154 mCurrentFragment = fragment; 155 156 if (vdebug()) { 157 Log.d(TAG, "setContentFragmentWithAnimations, fragmentName:" + mCurrentFragmentName); 158 } 159 } 160 handleIntent()161 private void handleIntent() { 162 Intent intent = getIntent(); 163 String action = intent != null ? intent.getAction() : null; 164 165 if (vdebug()) { 166 Log.d(TAG, "handleIntent, intent: " + intent + ", action: " + action); 167 } 168 169 if (action == null || action.length() == 0) { 170 return; 171 } 172 173 UiCall ringingCall; 174 switch (action) { 175 case ACTION_ANSWER_CALL: 176 ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING); 177 if (ringingCall == null) { 178 Log.e(TAG, "Unable to answer ringing call. There is none."); 179 } else { 180 mUiCallManager.answerCall(ringingCall); 181 } 182 break; 183 184 case ACTION_END_CALL: 185 ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING); 186 if (ringingCall == null) { 187 Log.e(TAG, "Unable to end ringing call. There is none."); 188 } else { 189 mUiCallManager.disconnectCall(ringingCall); 190 } 191 break; 192 193 case Intent.ACTION_DIAL: 194 String number = PhoneNumberUtils.getNumberFromIntent(intent, this); 195 if (!(mCurrentFragment instanceof NoHfpFragment)) { 196 showDialerWithNumber(number); 197 } 198 break; 199 200 default: 201 // Do nothing. 202 } 203 204 setIntent(null); 205 } 206 207 /** 208 * Will switch to the drawer or no-hfp fragment as necessary. 209 */ updateCurrentFragment()210 private void updateCurrentFragment() { 211 if (vdebug()) { 212 Log.d(TAG, "updateCurrentFragment"); 213 } 214 215 // TODO: do nothing when activity isFinishing() == true. 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 + mCurrentFragment); 230 } 231 232 if (ongoingCall == null && mCurrentFragment instanceof OngoingCallFragment) { 233 showSpeedDialFragment(); 234 } else if (ongoingCall != null) { 235 showOngoingCallFragment(); 236 } else if (DialerFragment.class.getSimpleName().equals(mCurrentFragmentName)) { 237 showDialer(); 238 } else { 239 showSpeedDialFragment(); 240 } 241 } 242 243 if (vdebug()) { 244 Log.d(TAG, "updateCurrentFragment: done"); 245 } 246 } 247 showSpeedDialFragment()248 private void showSpeedDialFragment() { 249 if (vdebug()) { 250 Log.d(TAG, "showSpeedDialFragment"); 251 } 252 253 if (mCurrentFragment instanceof StrequentsFragment) { 254 return; 255 } 256 257 if (mSpeedDialFragment == null) { 258 mSpeedDialFragment = new StrequentsFragment(); 259 Bundle args = new Bundle(); 260 mSpeedDialFragment.setArguments(args); 261 } 262 263 if (mCurrentFragment instanceof DialerFragment) { 264 setContentFragmentWithSlideAndDelayAnimation(mSpeedDialFragment); 265 } else { 266 setContentFragmentWithFadeAnimation(mSpeedDialFragment); 267 } 268 } 269 showOngoingCallFragment()270 private void showOngoingCallFragment() { 271 if (vdebug()) { 272 Log.d(TAG, "showOngoingCallFragment"); 273 } 274 if (mCurrentFragment instanceof OngoingCallFragment) { 275 closeDrawer(); 276 return; 277 } 278 279 if (mOngoingCallFragment == null) { 280 mOngoingCallFragment = new OngoingCallFragment(); 281 } 282 283 setContentFragmentWithFadeAnimation(mOngoingCallFragment); 284 closeDrawer(); 285 } 286 287 /** 288 * Displays the {@link DialerFragment} on top of the contents of the TelecomActivity. 289 */ showDialer()290 private void showDialer() { 291 if (vdebug()) { 292 Log.d(TAG, "showDialer"); 293 } 294 295 if (mDialerFragmentOpened) { 296 return; 297 } 298 299 if (mDialerFragment == null) { 300 if (Log.isLoggable(TAG, Log.VERBOSE)) { 301 Log.v(TAG, "showDialer: creating dialer"); 302 } 303 304 mDialerFragment = new DialerFragment(); 305 mDialerFragment.setDialerBackButtonListener(this); 306 } 307 308 if (Log.isLoggable(TAG, Log.VERBOSE)) { 309 Log.v(TAG, "adding dialer to fragment backstack"); 310 } 311 312 // Add the dialer fragment to the backstack so that it can be popped off to dismiss it. 313 getSupportFragmentManager().beginTransaction() 314 .setCustomAnimations(R.anim.telecom_slide_in, R.anim.telecom_slide_out, 315 R.anim.telecom_slide_in, R.anim.telecom_slide_out) 316 .add(getContentContainerId(), mDialerFragment) 317 .addToBackStack(DIALER_BACKSTACK) 318 .commitAllowingStateLoss(); 319 320 mDialerFragmentOpened = true; 321 322 if (Log.isLoggable(TAG, Log.VERBOSE)) { 323 Log.v(TAG, "done adding fragment to backstack"); 324 } 325 } 326 327 /** 328 * Checks if the dialpad fragment is opened and hides it if it is. 329 */ maybeHideDialer()330 private void maybeHideDialer() { 331 if (mDialerFragmentOpened) { 332 // Dismiss the dialer by removing it from the back stack. 333 getSupportFragmentManager().popBackStack(); 334 mDialerFragmentOpened = false; 335 } 336 } 337 338 @Override onDialerBackClick()339 public void onDialerBackClick() { 340 maybeHideDialer(); 341 } 342 showDialerWithNumber(String number)343 private void showDialerWithNumber(String number) { 344 showDialer(); 345 mDialerFragment.setDialNumber(number); 346 } 347 showNoHfpFragment(int stringResId)348 private void showNoHfpFragment(int stringResId) { 349 if (mCurrentFragment instanceof NoHfpFragment && stringResId == mLastNoHfpMessageId) { 350 return; 351 } 352 353 mLastNoHfpMessageId = stringResId; 354 String errorMessage = getString(stringResId); 355 NoHfpFragment frag = new NoHfpFragment(); 356 frag.setErrorMessage(errorMessage); 357 setContentFragment(frag); 358 mCurrentFragment = frag; 359 } 360 setContentFragmentWithSlideAndDelayAnimation(Fragment fragment)361 private void setContentFragmentWithSlideAndDelayAnimation(Fragment fragment) { 362 if (vdebug()) { 363 Log.d(TAG, "setContentFragmentWithSlideAndDelayAnimation, fragment: " + fragment); 364 } 365 setContentFragmentWithAnimations(fragment, 366 R.anim.telecom_slide_in_with_delay, R.anim.telecom_slide_out); 367 } 368 setContentFragmentWithFadeAnimation(Fragment fragment)369 private void setContentFragmentWithFadeAnimation(Fragment fragment) { 370 if (vdebug()) { 371 Log.d(TAG, "setContentFragmentWithFadeAnimation, fragment: " + fragment); 372 } 373 setContentFragmentWithAnimations(fragment, 374 R.anim.telecom_fade_in, R.anim.telecom_fade_out); 375 } 376 377 private final CallListener mCarCallListener = new UiCallManager.CallListener() { 378 @Override 379 public void onCallAdded(UiCall call) { 380 if (vdebug()) { 381 Log.d(TAG, "onCallAdded"); 382 } 383 updateCurrentFragment(); 384 } 385 386 @Override 387 public void onCallRemoved(UiCall call) { 388 if (vdebug()) { 389 Log.d(TAG, "onCallRemoved"); 390 } 391 updateCurrentFragment(); 392 } 393 394 @Override 395 public void onStateChanged(UiCall call, int state) { 396 if (vdebug()) { 397 Log.d(TAG, "onStateChanged"); 398 } 399 updateCurrentFragment(); 400 } 401 402 @Override 403 public void onCallUpdated(UiCall call) { 404 if (vdebug()) { 405 Log.d(TAG, "onCallUpdated"); 406 } 407 updateCurrentFragment(); 408 } 409 }; 410 setContentFragment(Fragment fragment)411 private void setContentFragment(Fragment fragment) { 412 getSupportFragmentManager().beginTransaction() 413 .replace(getContentContainerId(), fragment) 414 .commit(); 415 } 416 vdebug()417 private static boolean vdebug() { 418 return Log.isLoggable(TAG, Log.DEBUG); 419 } 420 421 @Override getRootAdapter()422 protected CarDrawerAdapter getRootAdapter() { 423 return new DialerRootAdapter(); 424 } 425 426 class CallLogAdapter extends CarDrawerAdapter { 427 private List<CallLogListingTask.CallLogItem> mItems; 428 CallLogAdapter(int titleResId, List<CallLogListingTask.CallLogItem> items)429 public CallLogAdapter(int titleResId, List<CallLogListingTask.CallLogItem> items) { 430 super(TelecomActivity.this, 431 true /* showDisabledListOnEmpty */, 432 false /* useSmallLayout */); 433 setTitle(getString(titleResId)); 434 mItems = items; 435 } 436 437 @Override getActualItemCount()438 protected int getActualItemCount() { 439 return mItems.size(); 440 } 441 442 @Override populateViewHolder(DrawerItemViewHolder holder, int position)443 public void populateViewHolder(DrawerItemViewHolder holder, int position) { 444 holder.getTitle().setText(mItems.get(position).mTitle); 445 holder.getText().setText(mItems.get(position).mText); 446 holder.getIcon().setImageBitmap(mItems.get(position).mIcon); 447 } 448 449 @Override onItemClick(int position)450 public void onItemClick(int position) { 451 closeDrawer(); 452 mUiCallManager.safePlaceCall(mItems.get(position).mNumber, false); 453 } 454 } 455 456 private class DialerRootAdapter extends CarDrawerAdapter { 457 private static final int ITEM_DIAL = 0; 458 private static final int ITEM_CALLLOG_ALL = 1; 459 private static final int ITEM_CALLLOG_MISSED = 2; 460 private static final int ITEM_MAX = 3; 461 DialerRootAdapter()462 DialerRootAdapter() { 463 super(TelecomActivity.this, 464 false /* showDisabledListOnEmpty */, 465 true /* useSmallLayout */); 466 setTitle(getString(R.string.phone_app_name)); 467 } 468 469 @Override getActualItemCount()470 protected int getActualItemCount() { 471 return ITEM_MAX; 472 } 473 474 @Override populateViewHolder(DrawerItemViewHolder holder, int position)475 public void populateViewHolder(DrawerItemViewHolder holder, int position) { 476 final int iconColor = getResources().getColor(R.color.car_tint); 477 int textResId, iconResId; 478 switch (position) { 479 case ITEM_DIAL: 480 textResId = R.string.calllog_dial_number; 481 iconResId = R.drawable.ic_drawer_dialpad; 482 break; 483 case ITEM_CALLLOG_ALL: 484 textResId = R.string.calllog_all; 485 iconResId = R.drawable.ic_drawer_history; 486 break; 487 case ITEM_CALLLOG_MISSED: 488 textResId = R.string.calllog_missed; 489 iconResId = R.drawable.ic_drawer_call_missed; 490 break; 491 default: 492 Log.wtf(TAG, "Unexpected position: " + position); 493 return; 494 } 495 holder.getTitle().setText(textResId); 496 Drawable drawable = getDrawable(iconResId); 497 drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); 498 holder.getIcon().setImageDrawable(drawable); 499 if (position > 0) { 500 drawable = getDrawable(R.drawable.ic_chevron_right); 501 drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); 502 holder.getRightIcon().setImageDrawable(drawable); 503 } 504 } 505 506 @Override onItemClick(int position)507 public void onItemClick(int position) { 508 switch (position) { 509 case ITEM_DIAL: 510 closeDrawer(); 511 showDialer(); 512 break; 513 case ITEM_CALLLOG_ALL: 514 loadCallHistoryAsync(PhoneLoader.CALL_TYPE_ALL, R.string.calllog_all); 515 break; 516 case ITEM_CALLLOG_MISSED: 517 loadCallHistoryAsync(PhoneLoader.CALL_TYPE_MISSED, R.string.calllog_missed); 518 break; 519 default: 520 Log.w(TAG, "Invalid position in ROOT menu! " + position); 521 } 522 } 523 } 524 loadCallHistoryAsync(final int callType, final int titleResId)525 private void loadCallHistoryAsync(final int callType, final int titleResId) { 526 showLoadingProgressBar(true); 527 // Warning: much callbackiness! 528 // First load up the call log cursor using the PhoneLoader so that happens in a 529 // background thread. TODO: Why isn't PhoneLoader using a LoaderManager? 530 PhoneLoader.registerCallObserver(callType, this, 531 (loader, data) -> { 532 // This callback runs on the thread that created the loader which is 533 // the ui thread so spin off another async task because we still need 534 // to pull together all the data along with the contact photo. 535 CallLogListingTask task = new CallLogListingTask(TelecomActivity.this, data, 536 (items) -> { 537 showLoadingProgressBar(false); 538 switchToAdapter(new CallLogAdapter(titleResId, items)); 539 }); 540 task.execute(); 541 }); 542 } 543 } 544