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