1 /* 2 * Copyright (C) 2019 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.car.dialer.ui; 18 19 import android.app.SearchManager; 20 import android.bluetooth.BluetoothDevice; 21 import android.content.Intent; 22 import android.content.SharedPreferences; 23 import android.graphics.drawable.Drawable; 24 import android.os.Bundle; 25 import android.provider.CallLog; 26 import android.telecom.Call; 27 import android.telephony.PhoneNumberUtils; 28 import android.view.View; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.fragment.app.Fragment; 33 import androidx.fragment.app.FragmentActivity; 34 import androidx.lifecycle.LiveData; 35 import androidx.lifecycle.MutableLiveData; 36 import androidx.lifecycle.ViewModelProviders; 37 import androidx.preference.PreferenceManager; 38 39 import com.android.car.apps.common.util.Themes; 40 import com.android.car.dialer.Constants; 41 import com.android.car.dialer.R; 42 import com.android.car.dialer.log.L; 43 import com.android.car.dialer.notification.NotificationService; 44 import com.android.car.dialer.telecom.UiCallManager; 45 import com.android.car.dialer.ui.activecall.InCallActivity; 46 import com.android.car.dialer.ui.activecall.InCallViewModel; 47 import com.android.car.dialer.ui.calllog.CallHistoryFragment; 48 import com.android.car.dialer.ui.common.DialerBaseFragment; 49 import com.android.car.dialer.ui.contact.ContactListFragment; 50 import com.android.car.dialer.ui.dialpad.DialpadFragment; 51 import com.android.car.dialer.ui.favorite.FavoriteFragment; 52 import com.android.car.dialer.ui.search.ContactResultsFragment; 53 import com.android.car.dialer.ui.settings.DialerSettingsActivity; 54 import com.android.car.dialer.ui.warning.NoHfpFragment; 55 import com.android.car.ui.toolbar.MenuItem; 56 import com.android.car.ui.toolbar.Toolbar; 57 58 import java.util.List; 59 60 /** 61 * Main activity for the Dialer app. It contains two layers: 62 * <ul> 63 * <li>Overlay layer for {@link NoHfpFragment} 64 * <li>Content layer for {@link FavoriteFragment} {@link CallHistoryFragment} {@link 65 * ContactListFragment} and {@link DialpadFragment} 66 * 67 * <p>Start {@link InCallActivity} if there are ongoing calls 68 * 69 * <p>Based on call and connectivity status, it will choose the right page to display. 70 */ 71 public class TelecomActivity extends FragmentActivity implements 72 DialerBaseFragment.DialerFragmentParent { 73 private static final String TAG = "CD.TelecomActivity"; 74 private LiveData<String> mBluetoothErrorMsgLiveData; 75 private LiveData<Integer> mDialerAppStateLiveData; 76 private LiveData<List<Call>> mOngoingCallListLiveData; 77 // View objects for this activity. 78 private TelecomPageTab.Factory mTabFactory; 79 private Toolbar mCarUiToolbar; 80 private BluetoothDevice mBluetoothDevice; 81 82 @Override onCreate(Bundle savedInstanceState)83 protected void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 86 L.d(TAG, "onCreate"); 87 setContentView(R.layout.telecom_activity); 88 89 mCarUiToolbar = findViewById(R.id.car_ui_toolbar); 90 91 setupTabLayout(); 92 93 TelecomActivityViewModel viewModel = ViewModelProviders.of(this).get( 94 TelecomActivityViewModel.class); 95 mBluetoothErrorMsgLiveData = viewModel.getErrorMessage(); 96 mDialerAppStateLiveData = viewModel.getDialerAppState(); 97 mDialerAppStateLiveData.observe(this, 98 dialerAppState -> updateCurrentFragment(dialerAppState)); 99 MutableLiveData<Integer> toolbarTitleMode = viewModel.getToolbarTitleMode(); 100 toolbarTitleMode.setValue(Themes.getAttrInteger(this, R.attr.toolbarTitleMode)); 101 viewModel.getRefreshTabsLiveData().observe(this, this::refreshTabs); 102 103 InCallViewModel inCallViewModel = ViewModelProviders.of(this).get(InCallViewModel.class); 104 mOngoingCallListLiveData = inCallViewModel.getOngoingCallList(); 105 // The mOngoingCallListLiveData needs to be active to get calculated. 106 mOngoingCallListLiveData.observe(this, this::maybeStartInCallActivity); 107 108 handleIntent(); 109 } 110 refreshTabs(boolean refreshTabs)111 private void refreshTabs(boolean refreshTabs) { 112 L.v(TAG, "hfp connected device list Changes."); 113 if (refreshTabs) { 114 setupTabLayout(); 115 } 116 } 117 118 @Override onNewIntent(Intent i)119 protected void onNewIntent(Intent i) { 120 super.onNewIntent(i); 121 setIntent(i); 122 handleIntent(); 123 } 124 handleIntent()125 private void handleIntent() { 126 Intent intent = getIntent(); 127 String action = intent != null ? intent.getAction() : null; 128 L.d(TAG, "handleIntent, intent: %s, action: %s", intent, action); 129 if (action == null || action.length() == 0) { 130 return; 131 } 132 133 String number; 134 switch (action) { 135 case Intent.ACTION_DIAL: 136 number = PhoneNumberUtils.getNumberFromIntent(intent, this); 137 if (TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR 138 != mDialerAppStateLiveData.getValue()) { 139 showDialPadFragment(number); 140 } 141 break; 142 143 case Intent.ACTION_CALL: 144 number = PhoneNumberUtils.getNumberFromIntent(intent, this); 145 UiCallManager.get().placeCall(number); 146 break; 147 148 case Intent.ACTION_SEARCH: 149 String searchQuery = intent.getStringExtra(SearchManager.QUERY); 150 navigateToContactResultsFragment(searchQuery); 151 break; 152 153 case Constants.Intents.ACTION_SHOW_PAGE: 154 if (TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR 155 != mDialerAppStateLiveData.getValue()) { 156 showTabPage(intent.getStringExtra(Constants.Intents.EXTRA_SHOW_PAGE)); 157 if (intent.getBooleanExtra(Constants.Intents.EXTRA_ACTION_READ_MISSED, false)) { 158 NotificationService.readAllMissedCall(this); 159 } 160 } 161 break; 162 case Intent.ACTION_VIEW: 163 if (CallLog.Calls.CONTENT_TYPE.equals(intent.getType())) { 164 if (TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR 165 != mDialerAppStateLiveData.getValue()) { 166 showTabPage(TelecomPageTab.Page.CALL_HISTORY); 167 } 168 } 169 break; 170 default: 171 // Do nothing. 172 } 173 174 setIntent(null); 175 176 // This is to start the incall activity when user taps on the dialer launch icon rapidly 177 maybeStartInCallActivity(mOngoingCallListLiveData.getValue()); 178 } 179 180 /** 181 * Update the current visible fragment of this Activity based on the state of the application. 182 * <ul> 183 * <li> If bluetooth is not connected or there is an active call, show overlay, lock drawer, 184 * hide action bar and hide the content layer. 185 * <li> Otherwise, show the content layer, show action bar, hide the overlay and reset drawer 186 * lock mode. 187 */ updateCurrentFragment( @elecomActivityViewModel.DialerAppState int dialerAppState)188 private void updateCurrentFragment( 189 @TelecomActivityViewModel.DialerAppState int dialerAppState) { 190 L.d(TAG, "updateCurrentFragment, dialerAppState: %d", dialerAppState); 191 192 boolean isOverlayFragmentVisible = 193 TelecomActivityViewModel.DialerAppState.DEFAULT != dialerAppState; 194 findViewById(R.id.content_container) 195 .setVisibility(isOverlayFragmentVisible ? View.GONE : View.VISIBLE); 196 findViewById(R.id.overlay_container) 197 .setVisibility(isOverlayFragmentVisible ? View.VISIBLE : View.GONE); 198 199 switch (dialerAppState) { 200 case TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR: 201 showNoHfpOverlay(mBluetoothErrorMsgLiveData.getValue()); 202 break; 203 204 case TelecomActivityViewModel.DialerAppState.EMERGENCY_DIALPAD: 205 setOverlayFragment(DialpadFragment.newEmergencyDialpad()); 206 break; 207 208 case TelecomActivityViewModel.DialerAppState.DEFAULT: 209 default: 210 clearOverlayFragment(); 211 break; 212 } 213 } 214 showNoHfpOverlay(String errorMsg)215 private void showNoHfpOverlay(String errorMsg) { 216 Fragment overlayFragment = getCurrentOverlayFragment(); 217 if (overlayFragment instanceof NoHfpFragment) { 218 ((NoHfpFragment) overlayFragment).setErrorMessage(errorMsg); 219 } else { 220 setOverlayFragment(NoHfpFragment.newInstance(errorMsg)); 221 } 222 } 223 setOverlayFragment(@onNull Fragment overlayFragment)224 private void setOverlayFragment(@NonNull Fragment overlayFragment) { 225 L.d(TAG, "setOverlayFragment: %s", overlayFragment); 226 227 getSupportFragmentManager() 228 .beginTransaction() 229 .replace(R.id.overlay_container, overlayFragment) 230 .commitNow(); 231 } 232 clearOverlayFragment()233 private void clearOverlayFragment() { 234 L.d(TAG, "clearOverlayFragment"); 235 236 Fragment overlayFragment = getCurrentOverlayFragment(); 237 if (overlayFragment == null) { 238 return; 239 } 240 241 getSupportFragmentManager() 242 .beginTransaction() 243 .remove(overlayFragment) 244 .commitNow(); 245 } 246 247 /** 248 * Returns the fragment that is currently being displayed as the overlay view on top. 249 */ 250 @Nullable getCurrentOverlayFragment()251 private Fragment getCurrentOverlayFragment() { 252 return getSupportFragmentManager().findFragmentById(R.id.overlay_container); 253 } 254 setupTabLayout()255 private void setupTabLayout() { 256 boolean wasContentFragmentRestored = false; 257 mTabFactory = new TelecomPageTab.Factory(this, getSupportFragmentManager()); 258 mCarUiToolbar.clearAllTabs(); 259 for (int i = 0; i < mTabFactory.getTabCount(); i++) { 260 TelecomPageTab tab = mTabFactory.createTab(getBaseContext(), i); 261 mCarUiToolbar.addTab(tab); 262 263 if (tab.wasFragmentRestored()) { 264 mCarUiToolbar.selectTab(i); 265 wasContentFragmentRestored = true; 266 } 267 } 268 269 // Select the starting tab and set up the fragment for it. 270 if (!wasContentFragmentRestored) { 271 int startTabIndex = getTabFromSharedPreference(); 272 TelecomPageTab startTab = (TelecomPageTab) mCarUiToolbar.getTab(startTabIndex); 273 mCarUiToolbar.selectTab(startTabIndex); 274 setContentFragment(startTab.getFragment(), startTab.getFragmentTag()); 275 } 276 277 mCarUiToolbar.registerOnTabSelectedListener( 278 tab -> { 279 TelecomPageTab telecomPageTab = (TelecomPageTab) tab; 280 Fragment fragment = telecomPageTab.getFragment(); 281 setContentFragment(fragment, telecomPageTab.getFragmentTag()); 282 }); 283 } 284 285 /** 286 * Switch to {@link DialpadFragment} and set the given number as dialed number. 287 */ showDialPadFragment(String number)288 private void showDialPadFragment(String number) { 289 int dialpadTabIndex = showTabPage(TelecomPageTab.Page.DIAL_PAD); 290 291 if (dialpadTabIndex == -1) { 292 return; 293 } 294 295 TelecomPageTab dialpadTab = (TelecomPageTab) mCarUiToolbar.getTab(dialpadTabIndex); 296 Fragment fragment = dialpadTab.getFragment(); 297 if (fragment instanceof DialpadFragment) { 298 ((DialpadFragment) fragment).setDialedNumber(number); 299 } else { 300 L.w(TAG, "Current tab is not a dialpad fragment!"); 301 } 302 } 303 showTabPage(@elecomPageTab.Page String tabPage)304 private int showTabPage(@TelecomPageTab.Page String tabPage) { 305 int tabIndex = mTabFactory.getTabIndex(tabPage); 306 if (tabIndex == -1) { 307 L.w(TAG, "Page %s is not a tab.", tabPage); 308 return -1; 309 } 310 getSupportFragmentManager().executePendingTransactions(); 311 while (isBackNavigationAvailable()) { 312 getSupportFragmentManager().popBackStackImmediate(); 313 } 314 315 mCarUiToolbar.selectTab(tabIndex); 316 return tabIndex; 317 } 318 setContentFragment(Fragment fragment, String fragmentTag)319 private void setContentFragment(Fragment fragment, String fragmentTag) { 320 L.d(TAG, "setContentFragment: %s", fragment); 321 322 getSupportFragmentManager().executePendingTransactions(); 323 while (getSupportFragmentManager().getBackStackEntryCount() > 0) { 324 getSupportFragmentManager().popBackStackImmediate(); 325 } 326 327 getSupportFragmentManager() 328 .beginTransaction() 329 .replace(R.id.content_fragment_container, fragment, fragmentTag) 330 .addToBackStack(fragmentTag) 331 .commit(); 332 } 333 334 @Override pushContentFragment(@onNull Fragment topContentFragment, String fragmentTag)335 public void pushContentFragment(@NonNull Fragment topContentFragment, String fragmentTag) { 336 L.d(TAG, "pushContentFragment: %s", topContentFragment); 337 338 getSupportFragmentManager() 339 .beginTransaction() 340 .replace(R.id.content_fragment_container, topContentFragment, fragmentTag) 341 .addToBackStack(fragmentTag) 342 .commit(); 343 } 344 345 @Override onNavigateUp()346 public boolean onNavigateUp() { 347 if (isBackNavigationAvailable()) { 348 onBackPressed(); 349 return true; 350 } 351 return super.onNavigateUp(); 352 } 353 354 @Override onBackPressed()355 public void onBackPressed() { 356 // By default onBackPressed will pop all the fragments off the backstack and then finish 357 // the activity. We want to finish the activity while there is still one fragment on the 358 // backstack, because we use onBackStackChanged() to set up our fragments. 359 if (isBackNavigationAvailable()) { 360 super.onBackPressed(); 361 } else { 362 finishAfterTransition(); 363 } 364 } 365 366 /** 367 * Handles the click action on the menu items. 368 */ onMenuItemClicked(MenuItem item)369 public void onMenuItemClicked(MenuItem item) { 370 switch (item.getId()) { 371 case R.id.menu_item_search: 372 Intent searchIntent = new Intent(getApplicationContext(), TelecomActivity.class); 373 searchIntent.setAction(Intent.ACTION_SEARCH); 374 startActivity(searchIntent); 375 break; 376 case R.id.menu_item_setting: 377 Intent settingsIntent = new Intent(getApplicationContext(), 378 DialerSettingsActivity.class); 379 startActivity(settingsIntent); 380 break; 381 } 382 } 383 navigateToContactResultsFragment(String query)384 private void navigateToContactResultsFragment(String query) { 385 Fragment topFragment = getSupportFragmentManager().findFragmentById( 386 R.id.content_fragment_container); 387 388 // Top fragment is ContactResultsFragment, update search query 389 if (topFragment instanceof ContactResultsFragment) { 390 ((ContactResultsFragment) topFragment).setSearchQuery(query); 391 return; 392 } 393 394 ContactResultsFragment fragment = ContactResultsFragment.newInstance(query); 395 pushContentFragment(fragment, ContactResultsFragment.FRAGMENT_TAG); 396 } 397 maybeStartInCallActivity(List<Call> callList)398 private void maybeStartInCallActivity(List<Call> callList) { 399 if (callList == null || callList.isEmpty()) { 400 return; 401 } 402 403 L.d(TAG, "Start InCallActivity"); 404 Intent launchIntent = new Intent(getApplicationContext(), InCallActivity.class); 405 startActivity(launchIntent); 406 } 407 408 /** 409 * If the back button on action bar is available to navigate up. 410 */ isBackNavigationAvailable()411 private boolean isBackNavigationAvailable() { 412 return getSupportFragmentManager().getBackStackEntryCount() > 1; 413 } 414 getTabFromSharedPreference()415 private int getTabFromSharedPreference() { 416 String key = getResources().getString(R.string.pref_start_page_key); 417 String defaultValue = getResources().getStringArray(R.array.tabs_config)[0]; 418 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 419 return mTabFactory.getTabIndex(sharedPreferences.getString(key, defaultValue)); 420 } 421 422 /** 423 * Sets the background of the Activity's tool bar to a {@link Drawable} 424 */ setShowToolbarBackground(boolean showToolbarBackground)425 public void setShowToolbarBackground(boolean showToolbarBackground) { 426 mCarUiToolbar.setBackgroundShown(showToolbarBackground); 427 } 428 } 429