blob: 43fce86af0bee832167717c2e31d9f21f7cfe875 [file] [log] [blame]
/*
* 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 android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar.LayoutParams;
import android.telecom.Call;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuInflater;
import com.android.car.app.CarDrawerActivity;
import com.android.car.app.CarDrawerAdapter;
import com.android.car.app.DrawerItemViewHolder;
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.telecom.UiCallManager.CallListener;
import java.util.List;
import static android.provider.Contacts.Intents.SEARCH_SUGGESTION_CLICKED;
import static android.support.v7.widget.Toolbar.LayoutParams.MATCH_PARENT;
/**
* Main activity for the Dialer app. Displays different fragments depending on call and
* connectivity status:
* <ul>
* <li>OngoingCallFragment
* <li>NoHfpFragment
* <li>DialerFragment
* <li>StrequentFragment
* </ul>
*/
public class TelecomActivity extends CarDrawerActivity implements
DialerFragment.DialerBackButtonListener {
private static final String TAG = "Em.TelecomActivity";
private static final String ACTION_ANSWER_CALL = "com.android.car.dialer.ANSWER_CALL";
private static final String ACTION_END_CALL = "com.android.car.dialer.END_CALL";
private static final String DIALER_BACKSTACK = "DialerBackstack";
private static final String FRAGMENT_CLASS_KEY = "FRAGMENT_CLASS_KEY";
private final UiBluetoothMonitor.Listener mBluetoothListener = this::updateCurrentFragment;
private UiCallManager mUiCallManager;
private UiBluetoothMonitor mUiBluetoothMonitor;
private Fragment mCurrentFragment;
private String mCurrentFragmentName;
private int mLastNoHfpMessageId;
private StrequentsFragment mSpeedDialFragment;
private Fragment mOngoingCallFragment;
private DialerFragment mDialerFragment;
private boolean mDialerFragmentOpened;
private SearchView mSearchView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (vdebug()) {
Log.d(TAG, "onCreate");
}
getWindow().getDecorView().setBackgroundColor(getColor(R.color.phone_theme));
setTitle(getString(R.string.phone_app_name));
mUiCallManager = new UiCallManager(this);
mUiBluetoothMonitor = new UiBluetoothMonitor(this);
if (savedInstanceState != null) {
mCurrentFragmentName = savedInstanceState.getString(FRAGMENT_CLASS_KEY);
}
if (vdebug()) {
Log.d(TAG, "onCreate done, mCurrentFragmentName: " + mCurrentFragmentName);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (vdebug()) {
Log.d(TAG, "onCreateOptionsMenu");
}
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.options_menu, menu);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
MenuItem searchItem = menu.findItem(R.id.search);
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
// The back arrow on the search view causes this to trigger. It isn't really a back
// at all so we can't use the back stack and instead we just set the speed dial
// fragment manually.
showSpeedDialFragment();
return true;
}
});
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
mSearchView = searchView;
return super.onCreateOptionsMenu(menu);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (vdebug()) {
Log.d(TAG, "onDestroy");
}
mUiBluetoothMonitor.tearDown();
mUiCallManager = null;
}
@Override
protected void onPause() {
super.onPause();
mUiCallManager.removeListener(mCarCallListener);
mUiBluetoothMonitor.removeListener(mBluetoothListener);
}
@Override
public void onSaveInstanceState(Bundle outState) {
if (mCurrentFragment != null) {
outState.putString(FRAGMENT_CLASS_KEY, mCurrentFragmentName);
}
super.onSaveInstanceState(outState);
}
@Override
protected void onNewIntent(Intent i) {
super.onNewIntent(i);
setIntent(i);
}
@Override
protected void onResume() {
if (vdebug()) {
Log.d(TAG, "onResume");
}
super.onResume();
// Update the current fragment before handling the intent so that any UI updates in
// handleIntent() is not overridden by updateCurrentFragment().
updateCurrentFragment();
handleIntent();
mUiCallManager.addListener(mCarCallListener);
mUiBluetoothMonitor.addListener(mBluetoothListener);
}
// TODO: move to base class.
private void setContentFragmentWithAnimations(Fragment fragment, int enter, int exit) {
if (vdebug()) {
Log.d(TAG, "setContentFragmentWithAnimations: " + fragment);
}
maybeHideDialer();
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(enter, exit)
.replace(getContentContainerId(), fragment)
.commitAllowingStateLoss();
mCurrentFragmentName = fragment.getClass().getSimpleName();
mCurrentFragment = fragment;
if (vdebug()) {
Log.d(TAG, "setContentFragmentWithAnimations, fragmentName:" + mCurrentFragmentName);
}
}
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;
}
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:
String number = PhoneNumberUtils.getNumberFromIntent(intent, this);
if (!(mCurrentFragment instanceof NoHfpFragment)) {
showDialerWithNumber(number);
}
break;
case SEARCH_SUGGESTION_CLICKED:
Uri contactUri = intent.getData();
showContactDetailFragment(contactUri);
break;
default:
// Do nothing.
}
setIntent(null);
}
/**
* Will switch to the drawer or no-hfp fragment as necessary.
*/
private void updateCurrentFragment() {
if (vdebug()) {
Log.d(TAG, "updateCurrentFragment");
}
// TODO: do nothing when activity isFinishing() == true.
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: "
+ mCurrentFragment);
}
if (ongoingCall == null && mCurrentFragment instanceof OngoingCallFragment) {
showSpeedDialFragment();
} else if (ongoingCall != null) {
showOngoingCallFragment();
} else if (DialerFragment.class.getSimpleName().equals(mCurrentFragmentName)) {
showDialer();
} else {
showSpeedDialFragment();
}
}
if (vdebug()) {
Log.d(TAG, "updateCurrentFragment: done");
}
}
private void showSpeedDialFragment() {
if (vdebug()) {
Log.d(TAG, "showSpeedDialFragment");
}
if (mCurrentFragment instanceof StrequentsFragment) {
return;
}
if (mSpeedDialFragment == null) {
mSpeedDialFragment = StrequentsFragment.newInstance(mUiCallManager);
Bundle args = new Bundle();
mSpeedDialFragment.setArguments(args);
}
if (mCurrentFragment instanceof DialerFragment) {
setContentFragmentWithSlideAndDelayAnimation(mSpeedDialFragment);
} else {
setContentFragmentWithFadeAnimation(mSpeedDialFragment);
}
}
private void showOngoingCallFragment() {
if (vdebug()) {
Log.d(TAG, "showOngoingCallFragment");
}
if (mCurrentFragment instanceof OngoingCallFragment) {
closeDrawer();
return;
}
if (mOngoingCallFragment == null) {
mOngoingCallFragment =
OngoingCallFragment.newInstance(mUiCallManager, mUiBluetoothMonitor);
}
setContentFragmentWithFadeAnimation(mOngoingCallFragment);
closeDrawer();
}
/**
* Displays the {@link DialerFragment} on top of the contents of the TelecomActivity.
*/
private void showDialer() {
if (vdebug()) {
Log.d(TAG, "showDialer");
}
if (mDialerFragmentOpened) {
return;
}
if (mDialerFragment == null) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "showDialer: creating dialer");
}
mDialerFragment = DialerFragment.newInstance(mUiCallManager);
mDialerFragment.setDialerBackButtonListener(this);
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "adding dialer to fragment backstack");
}
// Add the dialer fragment to the backstack so that it can be popped off to dismiss it.
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.telecom_slide_in, R.anim.telecom_slide_out,
R.anim.telecom_slide_in, R.anim.telecom_slide_out)
.add(getContentContainerId(), mDialerFragment)
.addToBackStack(DIALER_BACKSTACK)
.commitAllowingStateLoss();
mDialerFragmentOpened = true;
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "done adding fragment to backstack");
}
}
/**
* Checks if the dialpad fragment is opened and hides it if it is.
*/
private void maybeHideDialer() {
if (mDialerFragmentOpened) {
// Dismiss the dialer by removing it from the back stack.
getSupportFragmentManager().popBackStack();
mDialerFragmentOpened = false;
}
}
@Override
public void onDialerBackClick() {
maybeHideDialer();
}
private void showDialerWithNumber(String number) {
showDialer();
mDialerFragment.setDialNumber(number);
}
private void showNoHfpFragment(int stringResId) {
if (mCurrentFragment instanceof NoHfpFragment && stringResId == mLastNoHfpMessageId) {
return;
}
mLastNoHfpMessageId = stringResId;
String errorMessage = getString(stringResId);
NoHfpFragment frag = new NoHfpFragment();
frag.setErrorMessage(errorMessage);
setContentFragment(frag);
mCurrentFragment = frag;
}
private void showContactDetailFragment(Uri contactUri) {
ContactDetailsFragment fragment = ContactDetailsFragment.newInstance(contactUri);
setContentFragment(fragment);
mCurrentFragment = fragment;
}
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 final CallListener mCarCallListener = new UiCallManager.CallListener() {
@Override
public void onCallAdded(UiCall call) {
if (vdebug()) {
Log.d(TAG, "onCallAdded");
}
updateCurrentFragment();
}
@Override
public void onCallRemoved(UiCall call) {
if (vdebug()) {
Log.d(TAG, "onCallRemoved");
}
updateCurrentFragment();
}
@Override
public void onStateChanged(UiCall call, int state) {
if (vdebug()) {
Log.d(TAG, "onStateChanged");
}
updateCurrentFragment();
}
@Override
public void onCallUpdated(UiCall call) {
if (vdebug()) {
Log.d(TAG, "onCallUpdated");
}
updateCurrentFragment();
}
};
private void setContentFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.replace(getContentContainerId(), fragment)
.commit();
}
private static boolean vdebug() {
return Log.isLoggable(TAG, Log.DEBUG);
}
@Override
protected CarDrawerAdapter getRootAdapter() {
return new DialerRootAdapter();
}
class CallLogAdapter extends CarDrawerAdapter {
private List<CallLogListingTask.CallLogItem> mItems;
public CallLogAdapter(int titleResId, List<CallLogListingTask.CallLogItem> items) {
super(TelecomActivity.this, true /* showDisabledListOnEmpty */);
setTitle(getString(titleResId));
mItems = items;
}
@Override
protected boolean usesSmallLayout(int position) {
return false;
}
@Override
protected int getActualItemCount() {
return mItems.size();
}
@Override
public void populateViewHolder(DrawerItemViewHolder holder, int position) {
holder.getTitle().setText(mItems.get(position).mTitle);
holder.getText().setText(mItems.get(position).mText);
holder.getIcon().setImageBitmap(mItems.get(position).mIcon);
}
@Override
public void onItemClick(int position) {
closeDrawer();
mUiCallManager.safePlaceCall(mItems.get(position).mNumber, false);
}
}
private class DialerRootAdapter extends CarDrawerAdapter {
private static final int ITEM_DIAL = 0;
private static final int ITEM_CALLLOG_ALL = 1;
private static final int ITEM_CALLLOG_MISSED = 2;
private static final int ITEM_MAX = 3;
DialerRootAdapter() {
super(TelecomActivity.this, false /* showDisabledListOnEmpty */);
setTitle(getString(R.string.phone_app_name));
}
@Override
protected int getActualItemCount() {
return ITEM_MAX;
}
@Override
public void populateViewHolder(DrawerItemViewHolder holder, int position) {
final int iconColor = getResources().getColor(R.color.car_tint);
int textResId, iconResId;
switch (position) {
case ITEM_DIAL:
textResId = R.string.calllog_dial_number;
iconResId = R.drawable.ic_drawer_dialpad;
break;
case ITEM_CALLLOG_ALL:
textResId = R.string.calllog_all;
iconResId = R.drawable.ic_drawer_history;
break;
case ITEM_CALLLOG_MISSED:
textResId = R.string.calllog_missed;
iconResId = R.drawable.ic_drawer_call_missed;
break;
default:
Log.wtf(TAG, "Unexpected position: " + position);
return;
}
holder.getTitle().setText(textResId);
Drawable drawable = getDrawable(iconResId);
drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
holder.getIcon().setImageDrawable(drawable);
if (position > 0) {
drawable = getDrawable(R.drawable.ic_chevron_right);
drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
holder.getRightIcon().setImageDrawable(drawable);
}
}
@Override
public void onItemClick(int position) {
switch (position) {
case ITEM_DIAL:
closeDrawer();
showDialer();
break;
case ITEM_CALLLOG_ALL:
loadCallHistoryAsync(PhoneLoader.CALL_TYPE_ALL, R.string.calllog_all);
break;
case ITEM_CALLLOG_MISSED:
loadCallHistoryAsync(PhoneLoader.CALL_TYPE_MISSED, R.string.calllog_missed);
break;
default:
Log.w(TAG, "Invalid position in ROOT menu! " + position);
}
}
}
private void loadCallHistoryAsync(final int callType, final int titleResId) {
showLoadingProgressBar(true);
// Warning: much callbackiness!
// First load up the call log cursor using the PhoneLoader so that happens in a
// background thread. TODO: Why isn't PhoneLoader using a LoaderManager?
PhoneLoader.registerCallObserver(callType, this,
(loader, data) -> {
// This callback runs on the thread that created the loader which is
// the ui thread so spin off another async task because we still need
// to pull together all the data along with the contact photo.
CallLogListingTask task = new CallLogListingTask(TelecomActivity.this, data,
(items) -> {
showLoadingProgressBar(false);
switchToAdapter(new CallLogAdapter(titleResId, items));
});
task.execute();
});
}
}