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