/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.dialer; import static com.android.car.dialer.ui.CallHistoryFragment.CALL_TYPE_KEY; import android.content.Intent; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.app.Fragment; import android.telecom.Call; import android.telephony.PhoneNumberUtils; import android.util.Log; import androidx.car.drawer.CarDrawerActivity; import androidx.car.drawer.CarDrawerAdapter; import androidx.car.drawer.DrawerItemViewHolder; import com.android.car.dialer.telecom.InMemoryPhoneBook; import com.android.car.dialer.telecom.PhoneLoader; import com.android.car.dialer.telecom.UiCall; import com.android.car.dialer.telecom.UiCallManager; import com.android.car.dialer.ui.CallHistoryFragment; import com.android.car.dialer.ui.ContactListFragment; import com.android.car.dialer.ui.InCallFragment; import java.util.stream.Stream; /** * Main activity for the Dialer app. Displays different fragments depending on call and * connectivity status: *
The default value is {@code true} because it is only after
* {@link #onSaveInstanceState(Bundle)} that fragment commits are not allowed.
*/
private boolean mAllowFragmentCommits = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setToolbarElevation(0f);
if (vdebug()) {
Log.d(TAG, "onCreate");
}
setMainContent(R.layout.telecom_activity);
getWindow().getDecorView().setBackgroundColor(getColor(R.color.phone_theme));
updateTitle();
mUiCallManager = UiCallManager.init(getApplicationContext());
mUiBluetoothMonitor = new UiBluetoothMonitor(this);
InMemoryPhoneBook.init(getApplicationContext());
findViewById(R.id.search).setOnClickListener(
v -> startActivity(new Intent(this, ContactSearchActivity.class)));
getDrawerController().setRootAdapter(new DialerRootAdapter());
}
@Override
protected void onDestroy() {
super.onDestroy();
if (vdebug()) {
Log.d(TAG, "onDestroy");
}
mUiBluetoothMonitor.tearDown();
InMemoryPhoneBook.tearDown();
mUiCallManager.tearDown();
mUiCallManager = null;
}
@Override
protected void onStop() {
super.onStop();
mUiCallManager.removeListener(this);
mUiBluetoothMonitor.removeListener(mBluetoothListener);
}
@Override
public void onSaveInstanceState(Bundle outState) {
// A transaction can only be committed with this method prior to its containing activity
// saving its state.
mAllowFragmentCommits = false;
super.onSaveInstanceState(outState);
}
@Override
protected void onNewIntent(Intent i) {
super.onNewIntent(i);
setIntent(i);
}
@Override
protected void onStart() {
if (vdebug()) {
Log.d(TAG, "onStart");
}
super.onStart();
// Fragment commits are not allowed once the Activity's state has been saved. Once
// onStart() has been called, the FragmentManager should now allow commits.
mAllowFragmentCommits = true;
// Update the current fragment before handling the intent so that any UI updates in
// handleIntent() is not overridden by updateCurrentFragment().
updateCurrentFragment();
handleIntent();
mUiCallManager.addListener(this);
mUiBluetoothMonitor.addListener(mBluetoothListener);
}
private void handleIntent() {
Intent intent = getIntent();
String action = intent != null ? intent.getAction() : null;
if (vdebug()) {
Log.d(TAG, "handleIntent, intent: " + intent + ", action: " + action);
}
if (action == null || action.length() == 0) {
return;
}
String number;
UiCall ringingCall;
switch (action) {
case ACTION_ANSWER_CALL:
ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING);
if (ringingCall == null) {
Log.e(TAG, "Unable to answer ringing call. There is none.");
} else {
mUiCallManager.answerCall(ringingCall);
}
break;
case ACTION_END_CALL:
ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING);
if (ringingCall == null) {
Log.e(TAG, "Unable to end ringing call. There is none.");
} else {
mUiCallManager.disconnectCall(ringingCall);
}
break;
case Intent.ACTION_DIAL:
number = PhoneNumberUtils.getNumberFromIntent(intent, this);
if (!(getCurrentFragment() instanceof NoHfpFragment)) {
showDialer(number);
}
break;
case Intent.ACTION_CALL:
number = PhoneNumberUtils.getNumberFromIntent(intent, this);
mUiCallManager.safePlaceCall(number, false /* bluetoothRequired */);
break;
default:
// Do nothing.
}
setIntent(null);
}
/**
* Updates the content fragment of this Activity based on the state of the application.
*/
private void updateCurrentFragment() {
if (vdebug()) {
Log.d(TAG, "updateCurrentFragment()");
}
boolean callEmpty = mUiCallManager.getCalls().isEmpty();
if (!mUiBluetoothMonitor.isBluetoothEnabled() && callEmpty) {
showNoHfpFragment(R.string.bluetooth_disabled);
} else if (!mUiBluetoothMonitor.isBluetoothPaired() && callEmpty) {
showNoHfpFragment(R.string.bluetooth_unpaired);
} else if (!mUiBluetoothMonitor.isHfpConnected() && callEmpty) {
showNoHfpFragment(R.string.no_hfp);
} else {
UiCall ongoingCall = mUiCallManager.getPrimaryCall();
if (vdebug()) {
Log.d(TAG, "ongoingCall: " + ongoingCall + ", mCurrentFragment: "
+ getCurrentFragment());
}
if (ongoingCall == null && getCurrentFragment() instanceof InCallFragment) {
showSpeedDialFragment();
} else if (ongoingCall != null) {
showOngoingCallFragment();
} else {
showSpeedDialFragment();
}
}
if (vdebug()) {
Log.d(TAG, "updateCurrentFragment: done");
}
}
private void showSpeedDialFragment() {
if (vdebug()) {
Log.d(TAG, "showSpeedDialFragment");
}
if (!mAllowFragmentCommits || getCurrentFragment() instanceof StrequentsFragment) {
return;
}
Fragment fragment = StrequentsFragment.newInstance();
setContentFragment(fragment);
}
private void showOngoingCallFragment() {
if (vdebug()) {
Log.d(TAG, "showOngoingCallFragment");
}
if (!mAllowFragmentCommits || getCurrentFragment() instanceof InCallFragment) {
// in case the dialer is still open, (e.g. when dialing the second phone during
// a phone call), close it
maybeHideDialer();
getDrawerController().closeDrawer();
return;
}
Fragment fragment = InCallFragment.newInstance();
setContentFragmentWithFadeAnimation(fragment);
getDrawerController().closeDrawer();
}
private void showDialer() {
if (vdebug()) {
Log.d(TAG, "showDialer");
}
showDialer(null /* dialNumber */);
}
/**
* Displays the {@link DialerFragment} and initialize it with the given phone number.
*/
private void showDialer(@Nullable String dialNumber) {
if (vdebug()) {
Log.d(TAG, "showDialer with number: " + dialNumber);
}
if (!mAllowFragmentCommits ||
getSupportFragmentManager().findFragmentByTag(DIALER_FRAGMENT_TAG) != null) {
return;
}
Fragment fragment = DialerFragment.newInstance(dialNumber);
// Add the dialer fragment to the backstack so that it can be popped off to dismiss it.
setContentFragment(fragment);
}
/**
* Checks if the dialpad fragment is opened and hides it if it is.
*/
private void maybeHideDialer() {
// The dialer is the only fragment to be added to the back stack. Dismiss the dialer by
// removing it from the back stack.
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
getSupportFragmentManager().popBackStack();
}
}
private void showNoHfpFragment(@StringRes int stringResId) {
if (!mAllowFragmentCommits) {
return;
}
String errorMessage = getString(stringResId);
Fragment currentFragment = getCurrentFragment();
if (currentFragment instanceof NoHfpFragment) {
((NoHfpFragment) currentFragment).setErrorMessage(errorMessage);
} else {
setContentFragment(NoHfpFragment.newInstance(errorMessage));
}
}
private void setContentFragmentWithSlideAndDelayAnimation(Fragment fragment) {
if (vdebug()) {
Log.d(TAG, "setContentFragmentWithSlideAndDelayAnimation, fragment: " + fragment);
}
setContentFragmentWithAnimations(fragment,
R.anim.telecom_slide_in_with_delay, R.anim.telecom_slide_out);
}
private void setContentFragmentWithFadeAnimation(Fragment fragment) {
if (vdebug()) {
Log.d(TAG, "setContentFragmentWithFadeAnimation, fragment: " + fragment);
}
setContentFragmentWithAnimations(fragment,
R.anim.telecom_fade_in, R.anim.telecom_fade_out);
}
private void setContentFragmentWithAnimations(Fragment fragment, int enter, int exit) {
if (vdebug()) {
Log.d(TAG, "setContentFragmentWithAnimations: " + fragment);
}
maybeHideDialer();
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(enter, exit)
.replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
.commitNow();
}
/**
* Sets the fragment that will be shown as the main content of this Activity. Note that this
* fragment is not always visible. In particular, the dialer fragment can show up on top of this
* fragment.
*/
private void setContentFragment(Fragment fragment) {
maybeHideDialer();
getSupportFragmentManager().beginTransaction()
.replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
.commitNow();
updateTitle();
}
/**
* Returns the fragment that is currently being displayed as the content view. Note that this
* is not necessarily the fragment that is visible. For example, the returned fragment
* could be the content, but the dial fragment is being displayed on top of it. Check for
* the existence of the dial fragment with the TAG {@link #DIALER_FRAGMENT_TAG}.
*/
@Nullable
private Fragment getCurrentFragment() {
return getSupportFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG);
}
private static boolean vdebug() {
return Log.isLoggable(TAG, Log.DEBUG);
}
@Override
public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) {
fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
.onAudioStateChanged(isMuted, route, supportedRouteMask));
}
@Override
public void onCallStateChanged(UiCall call, int state) {
if (vdebug()) {
Log.d(TAG, "onCallStateChanged");
}
updateCurrentFragment();
fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
.onCallStateChanged(call, state));
}
@Override
public void onCallUpdated(UiCall call) {
if (vdebug()) {
Log.d(TAG, "onCallUpdated");
}
updateCurrentFragment();
fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
.onCallUpdated(call));
}
@Override
public void onCallAdded(UiCall call) {
if (vdebug()) {
Log.d(TAG, "onCallAdded");
}
updateCurrentFragment();
fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
.onCallAdded(call));
}
@Override
public void onCallRemoved(UiCall call) {
if (vdebug()) {
Log.d(TAG, "onCallRemoved");
}
updateCurrentFragment();
fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
.onCallRemoved(call));
}
private static boolean shouldPropagateCallback(Fragment fragment) {
return fragment instanceof CallListener && fragment.isAdded();
}
private Stream