• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.car.dialer.ui.CallHistoryFragment.CALL_TYPE_KEY;
19 
20 import android.content.Intent;
21 import android.graphics.PorterDuff;
22 import android.graphics.drawable.Drawable;
23 import android.os.Bundle;
24 import android.support.annotation.Nullable;
25 import android.support.annotation.StringRes;
26 import android.support.v4.app.Fragment;
27 import android.telecom.Call;
28 import android.telephony.PhoneNumberUtils;
29 import android.util.Log;
30 
31 import androidx.car.drawer.CarDrawerActivity;
32 import androidx.car.drawer.CarDrawerAdapter;
33 import androidx.car.drawer.DrawerItemViewHolder;
34 
35 import com.android.car.dialer.telecom.InMemoryPhoneBook;
36 import com.android.car.dialer.telecom.PhoneLoader;
37 import com.android.car.dialer.telecom.UiCall;
38 import com.android.car.dialer.telecom.UiCallManager;
39 import com.android.car.dialer.ui.CallHistoryFragment;
40 import com.android.car.dialer.ui.ContactListFragment;
41 import com.android.car.dialer.ui.InCallFragment;
42 
43 import java.util.stream.Stream;
44 
45 /**
46  * Main activity for the Dialer app. Displays different fragments depending on call and
47  * connectivity status:
48  * <ul>
49  * <li>OngoingCallFragment
50  * <li>NoHfpFragment
51  * <li>DialerFragment
52  * <li>StrequentFragment
53  * </ul>
54  */
55 public class TelecomActivity extends CarDrawerActivity implements CallListener {
56     private static final String TAG = "TelecomActivity";
57 
58     private static final String ACTION_ANSWER_CALL = "com.android.car.dialer.ANSWER_CALL";
59     private static final String ACTION_END_CALL = "com.android.car.dialer.END_CALL";
60 
61     private static final String DIALER_BACKSTACK = "DialerBackstack";
62     private static final String CONTENT_FRAGMENT_TAG = "CONTENT_FRAGMENT_TAG";
63     private static final String DIALER_FRAGMENT_TAG = "DIALER_FRAGMENT_TAG";
64 
65     private final UiBluetoothMonitor.Listener mBluetoothListener = this::updateCurrentFragment;
66 
67     private UiCallManager mUiCallManager;
68     private UiBluetoothMonitor mUiBluetoothMonitor;
69 
70     /**
71      * Whether or not it is safe to make transactions on the
72      * {@link android.support.v4.app.FragmentManager}. This variable prevents a possible exception
73      * when calling commit() on the FragmentManager.
74      *
75      * <p>The default value is {@code true} because it is only after
76      * {@link #onSaveInstanceState(Bundle)} that fragment commits are not allowed.
77      */
78     private boolean mAllowFragmentCommits = true;
79 
80     @Override
onCreate(Bundle savedInstanceState)81     protected void onCreate(Bundle savedInstanceState) {
82         super.onCreate(savedInstanceState);
83         setToolbarElevation(0f);
84 
85         if (vdebug()) {
86             Log.d(TAG, "onCreate");
87         }
88 
89         setMainContent(R.layout.telecom_activity);
90         getWindow().getDecorView().setBackgroundColor(getColor(R.color.phone_theme));
91         updateTitle();
92 
93         mUiCallManager = UiCallManager.init(getApplicationContext());
94         mUiBluetoothMonitor = new UiBluetoothMonitor(this);
95 
96         InMemoryPhoneBook.init(getApplicationContext());
97 
98         findViewById(R.id.search).setOnClickListener(
99                 v -> startActivity(new Intent(this, ContactSearchActivity.class)));
100 
101         getDrawerController().setRootAdapter(new DialerRootAdapter());
102     }
103 
104     @Override
onDestroy()105     protected void onDestroy() {
106         super.onDestroy();
107         if (vdebug()) {
108             Log.d(TAG, "onDestroy");
109         }
110         mUiBluetoothMonitor.tearDown();
111         InMemoryPhoneBook.tearDown();
112         mUiCallManager.tearDown();
113         mUiCallManager = null;
114     }
115 
116     @Override
onStop()117     protected void onStop() {
118         super.onStop();
119         mUiCallManager.removeListener(this);
120         mUiBluetoothMonitor.removeListener(mBluetoothListener);
121     }
122 
123     @Override
onSaveInstanceState(Bundle outState)124     public void onSaveInstanceState(Bundle outState) {
125         // A transaction can only be committed with this method prior to its containing activity
126         // saving its state.
127         mAllowFragmentCommits = false;
128         super.onSaveInstanceState(outState);
129     }
130 
131     @Override
onNewIntent(Intent i)132     protected void onNewIntent(Intent i) {
133         super.onNewIntent(i);
134         setIntent(i);
135     }
136 
137     @Override
onStart()138     protected void onStart() {
139         if (vdebug()) {
140             Log.d(TAG, "onStart");
141         }
142         super.onStart();
143 
144         // Fragment commits are not allowed once the Activity's state has been saved. Once
145         // onStart() has been called, the FragmentManager should now allow commits.
146         mAllowFragmentCommits = true;
147 
148         // Update the current fragment before handling the intent so that any UI updates in
149         // handleIntent() is not overridden by updateCurrentFragment().
150         updateCurrentFragment();
151         handleIntent();
152 
153         mUiCallManager.addListener(this);
154         mUiBluetoothMonitor.addListener(mBluetoothListener);
155     }
156 
handleIntent()157     private void handleIntent() {
158         Intent intent = getIntent();
159         String action = intent != null ? intent.getAction() : null;
160 
161         if (vdebug()) {
162             Log.d(TAG, "handleIntent, intent: " + intent + ", action: " + action);
163         }
164 
165         if (action == null || action.length() == 0) {
166             return;
167         }
168 
169         String number;
170         UiCall ringingCall;
171         switch (action) {
172             case ACTION_ANSWER_CALL:
173                 ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING);
174                 if (ringingCall == null) {
175                     Log.e(TAG, "Unable to answer ringing call. There is none.");
176                 } else {
177                     mUiCallManager.answerCall(ringingCall);
178                 }
179                 break;
180 
181             case ACTION_END_CALL:
182                 ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING);
183                 if (ringingCall == null) {
184                     Log.e(TAG, "Unable to end ringing call. There is none.");
185                 } else {
186                     mUiCallManager.disconnectCall(ringingCall);
187                 }
188                 break;
189 
190             case Intent.ACTION_DIAL:
191                 number = PhoneNumberUtils.getNumberFromIntent(intent, this);
192                 if (!(getCurrentFragment() instanceof NoHfpFragment)) {
193                     showDialer(number);
194                 }
195                 break;
196 
197             case Intent.ACTION_CALL:
198                 number = PhoneNumberUtils.getNumberFromIntent(intent, this);
199                 mUiCallManager.safePlaceCall(number, false /* bluetoothRequired */);
200                 break;
201 
202             default:
203                 // Do nothing.
204         }
205 
206         setIntent(null);
207     }
208 
209     /**
210      * Updates the content fragment of this Activity based on the state of the application.
211      */
updateCurrentFragment()212     private void updateCurrentFragment() {
213         if (vdebug()) {
214             Log.d(TAG, "updateCurrentFragment()");
215         }
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                         + getCurrentFragment());
230             }
231 
232             if (ongoingCall == null && getCurrentFragment() instanceof InCallFragment) {
233                 showSpeedDialFragment();
234             } else if (ongoingCall != null) {
235                 showOngoingCallFragment();
236             } else {
237                 showSpeedDialFragment();
238             }
239         }
240 
241         if (vdebug()) {
242             Log.d(TAG, "updateCurrentFragment: done");
243         }
244     }
245 
showSpeedDialFragment()246     private void showSpeedDialFragment() {
247         if (vdebug()) {
248             Log.d(TAG, "showSpeedDialFragment");
249         }
250 
251         if (!mAllowFragmentCommits || getCurrentFragment() instanceof StrequentsFragment) {
252             return;
253         }
254 
255         Fragment fragment = StrequentsFragment.newInstance();
256         setContentFragment(fragment);
257     }
258 
showOngoingCallFragment()259     private void showOngoingCallFragment() {
260         if (vdebug()) {
261             Log.d(TAG, "showOngoingCallFragment");
262         }
263         if (!mAllowFragmentCommits || getCurrentFragment() instanceof InCallFragment) {
264             // in case the dialer is still open, (e.g. when dialing the second phone during
265             // a phone call), close it
266             maybeHideDialer();
267             getDrawerController().closeDrawer();
268             return;
269         }
270         Fragment fragment = InCallFragment.newInstance();
271         setContentFragmentWithFadeAnimation(fragment);
272         getDrawerController().closeDrawer();
273     }
274 
showDialer()275     private void showDialer() {
276         if (vdebug()) {
277             Log.d(TAG, "showDialer");
278         }
279 
280         showDialer(null /* dialNumber */);
281     }
282 
283     /**
284      * Displays the {@link DialerFragment} and initialize it with the given phone number.
285      */
showDialer(@ullable String dialNumber)286     private void showDialer(@Nullable String dialNumber) {
287         if (vdebug()) {
288             Log.d(TAG, "showDialer with number: " + dialNumber);
289         }
290 
291         if (!mAllowFragmentCommits ||
292                 getSupportFragmentManager().findFragmentByTag(DIALER_FRAGMENT_TAG) != null) {
293             return;
294         }
295 
296         Fragment fragment = DialerFragment.newInstance(dialNumber);
297         // Add the dialer fragment to the backstack so that it can be popped off to dismiss it.
298         setContentFragment(fragment);
299     }
300 
301     /**
302      * Checks if the dialpad fragment is opened and hides it if it is.
303      */
maybeHideDialer()304     private void maybeHideDialer() {
305         // The dialer is the only fragment to be added to the back stack. Dismiss the dialer by
306         // removing it from the back stack.
307         if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
308             getSupportFragmentManager().popBackStack();
309         }
310     }
311 
showNoHfpFragment(@tringRes int stringResId)312     private void showNoHfpFragment(@StringRes int stringResId) {
313         if (!mAllowFragmentCommits) {
314             return;
315         }
316 
317         String errorMessage = getString(stringResId);
318         Fragment currentFragment = getCurrentFragment();
319 
320         if (currentFragment instanceof NoHfpFragment) {
321             ((NoHfpFragment) currentFragment).setErrorMessage(errorMessage);
322         } else {
323             setContentFragment(NoHfpFragment.newInstance(errorMessage));
324         }
325     }
326 
setContentFragmentWithSlideAndDelayAnimation(Fragment fragment)327     private void setContentFragmentWithSlideAndDelayAnimation(Fragment fragment) {
328         if (vdebug()) {
329             Log.d(TAG, "setContentFragmentWithSlideAndDelayAnimation, fragment: " + fragment);
330         }
331         setContentFragmentWithAnimations(fragment,
332                 R.anim.telecom_slide_in_with_delay, R.anim.telecom_slide_out);
333     }
334 
setContentFragmentWithFadeAnimation(Fragment fragment)335     private void setContentFragmentWithFadeAnimation(Fragment fragment) {
336         if (vdebug()) {
337             Log.d(TAG, "setContentFragmentWithFadeAnimation, fragment: " + fragment);
338         }
339         setContentFragmentWithAnimations(fragment,
340                 R.anim.telecom_fade_in, R.anim.telecom_fade_out);
341     }
342 
setContentFragmentWithAnimations(Fragment fragment, int enter, int exit)343     private void setContentFragmentWithAnimations(Fragment fragment, int enter, int exit) {
344         if (vdebug()) {
345             Log.d(TAG, "setContentFragmentWithAnimations: " + fragment);
346         }
347 
348         maybeHideDialer();
349 
350         getSupportFragmentManager().beginTransaction()
351                 .setCustomAnimations(enter, exit)
352                 .replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
353                 .commitNow();
354     }
355 
356     /**
357      * Sets the fragment that will be shown as the main content of this Activity. Note that this
358      * fragment is not always visible. In particular, the dialer fragment can show up on top of this
359      * fragment.
360      */
setContentFragment(Fragment fragment)361     private void setContentFragment(Fragment fragment) {
362         maybeHideDialer();
363         getSupportFragmentManager().beginTransaction()
364                 .replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
365                 .commitNow();
366         updateTitle();
367     }
368 
369     /**
370      * Returns the fragment that is currently being displayed as the content view. Note that this
371      * is not necessarily the fragment that is visible. For example, the returned fragment
372      * could be the content, but the dial fragment is being displayed on top of it. Check for
373      * the existence of the dial fragment with the TAG {@link #DIALER_FRAGMENT_TAG}.
374      */
375     @Nullable
getCurrentFragment()376     private Fragment getCurrentFragment() {
377         return getSupportFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG);
378     }
379 
vdebug()380     private static boolean vdebug() {
381         return Log.isLoggable(TAG, Log.DEBUG);
382     }
383 
384     @Override
onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask)385     public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) {
386         fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
387                 .onAudioStateChanged(isMuted, route, supportedRouteMask));
388     }
389 
390     @Override
onCallStateChanged(UiCall call, int state)391     public void onCallStateChanged(UiCall call, int state) {
392         if (vdebug()) {
393             Log.d(TAG, "onCallStateChanged");
394         }
395         updateCurrentFragment();
396 
397         fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
398                 .onCallStateChanged(call, state));
399     }
400 
401     @Override
onCallUpdated(UiCall call)402     public void onCallUpdated(UiCall call) {
403         if (vdebug()) {
404             Log.d(TAG, "onCallUpdated");
405         }
406         updateCurrentFragment();
407 
408         fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
409                 .onCallUpdated(call));
410     }
411 
412     @Override
onCallAdded(UiCall call)413     public void onCallAdded(UiCall call) {
414         if (vdebug()) {
415             Log.d(TAG, "onCallAdded");
416         }
417         updateCurrentFragment();
418 
419         fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
420                 .onCallAdded(call));
421     }
422 
423     @Override
onCallRemoved(UiCall call)424     public void onCallRemoved(UiCall call) {
425         if (vdebug()) {
426             Log.d(TAG, "onCallRemoved");
427         }
428         updateCurrentFragment();
429 
430         fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
431                 .onCallRemoved(call));
432     }
433 
shouldPropagateCallback(Fragment fragment)434     private static boolean shouldPropagateCallback(Fragment fragment) {
435         return fragment instanceof CallListener && fragment.isAdded();
436     }
437 
fragmentsToPropagateCallback()438     private Stream<Fragment> fragmentsToPropagateCallback() {
439         return getSupportFragmentManager().getFragments().stream()
440                 .filter(fragment -> shouldPropagateCallback(fragment));
441     }
442 
443     private class DialerRootAdapter extends CarDrawerAdapter {
444         private static final int ITEM_FAVORITES = 0;
445         private static final int ITEM_CALLLOG_ALL = 1;
446         private static final int ITEM_CALLLOG_MISSED = 2;
447         private static final int ITEM_CONTACT = 3;
448         private static final int ITEM_DIAL = 4;
449 
450         private static final int ITEM_COUNT = 5;
451 
DialerRootAdapter()452         DialerRootAdapter() {
453             super(TelecomActivity.this, false /* showDisabledListOnEmpty */);
454         }
455 
456         @Override
getActualItemCount()457         protected int getActualItemCount() {
458             return ITEM_COUNT;
459         }
460 
461         @Override
populateViewHolder(DrawerItemViewHolder holder, int position)462         public void populateViewHolder(DrawerItemViewHolder holder, int position) {
463             final int iconColor = getResources().getColor(R.color.car_tint);
464             int textResId, iconResId;
465             switch (position) {
466                 case ITEM_DIAL:
467                     textResId = R.string.calllog_dial_number;
468                     iconResId = R.drawable.ic_drawer_dialpad;
469                     break;
470                 case ITEM_CALLLOG_ALL:
471                     textResId = R.string.calllog_all;
472                     iconResId = R.drawable.ic_drawer_history;
473                     break;
474                 case ITEM_CALLLOG_MISSED:
475                     textResId = R.string.calllog_missed;
476                     iconResId = R.drawable.ic_call_missed;
477                     break;
478                 case ITEM_FAVORITES:
479                     textResId = R.string.calllog_favorites;
480                     iconResId = R.drawable.ic_favorite;
481                     break;
482                 case ITEM_CONTACT:
483                     textResId = R.string.contact_menu_label;
484                     iconResId = R.drawable.ic_contact;
485                     break;
486                 default:
487                     Log.wtf(TAG, "Unexpected position: " + position);
488                     return;
489             }
490             holder.getTitle().setText(textResId);
491             Drawable drawable = getDrawable(iconResId);
492             drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
493             holder.getIcon().setImageDrawable(drawable);
494         }
495 
496         @Override
onItemClick(int position)497         public void onItemClick(int position) {
498             getDrawerController().closeDrawer();
499             switch (position) {
500                 case ITEM_DIAL:
501                     showDialer();
502                     break;
503                 case ITEM_CALLLOG_ALL:
504                     showCallHistory(PhoneLoader.CallType.CALL_TYPE_ALL);
505                     break;
506                 case ITEM_CALLLOG_MISSED:
507                     showCallHistory(PhoneLoader.CallType.MISSED_TYPE);
508                     break;
509                 case ITEM_FAVORITES:
510                     showSpeedDialFragment();
511                     break;
512                 case ITEM_CONTACT:
513                     showContact();
514                     break;
515                 default:
516                     Log.w(TAG, "Invalid position in ROOT menu! " + position);
517             }
518             setTitle(getTitleString());
519         }
520     }
521 
showCallHistory(@honeLoader.CallType int callType)522     private void showCallHistory(@PhoneLoader.CallType int callType) {
523         setContentFragment(CallHistoryFragment.newInstance(callType));
524     }
525 
showContact()526     private void showContact() {
527         setContentFragment(ContactListFragment.newInstance());
528     }
529 
updateTitle()530     private void updateTitle() {
531         setTitle(getTitleString());
532     }
533 
getTitleString()534     private String getTitleString() {
535         Fragment currentFragment = getCurrentFragment();
536 
537         int titleResId = R.string.phone_app_name;
538 
539         if (currentFragment instanceof StrequentsFragment) {
540             titleResId = R.string.contacts_title;
541         } else if (currentFragment instanceof CallHistoryFragment) {
542             int callType = currentFragment.getArguments().getInt(CALL_TYPE_KEY);
543             if (callType == PhoneLoader.CallType.MISSED_TYPE) {
544                 titleResId = R.string.missed_call_title;
545             } else {
546                 titleResId = R.string.call_history_title;
547             }
548         } else if (currentFragment instanceof ContactListFragment) {
549             titleResId = R.string.contacts_title;
550         } else if (currentFragment instanceof DialerFragment) {
551             titleResId = R.string.dialpad_title;
552         } else if (currentFragment instanceof InCallFragment
553                 || currentFragment instanceof OngoingCallFragment) {
554             titleResId = R.string.in_call_title;
555         }
556 
557         return getString(titleResId);
558     }
559 }
560