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.os.Bundle;
24 import android.provider.CallLog;
25 import android.telecom.Call;
26 import android.telephony.PhoneNumberUtils;
27 
28 import androidx.annotation.NonNull;
29 import androidx.fragment.app.Fragment;
30 import androidx.fragment.app.FragmentActivity;
31 import androidx.lifecycle.LiveData;
32 import androidx.lifecycle.MutableLiveData;
33 import androidx.lifecycle.ViewModelProviders;
34 import androidx.preference.PreferenceManager;
35 
36 import com.android.car.apps.common.util.Themes;
37 import com.android.car.dialer.Constants;
38 import com.android.car.dialer.R;
39 import com.android.car.dialer.livedata.BluetoothErrorStringLiveData;
40 import com.android.car.dialer.log.L;
41 import com.android.car.dialer.notification.NotificationService;
42 import com.android.car.dialer.telecom.UiCallManager;
43 import com.android.car.dialer.ui.activecall.InCallActivity;
44 import com.android.car.dialer.ui.activecall.InCallViewModel;
45 import com.android.car.dialer.ui.common.DialerBaseFragment;
46 import com.android.car.dialer.ui.dialpad.DialpadFragment;
47 import com.android.car.dialer.ui.search.ContactResultsFragment;
48 import com.android.car.dialer.ui.settings.DialerSettingsActivity;
49 import com.android.car.ui.baselayout.Insets;
50 import com.android.car.ui.baselayout.InsetsChangedListener;
51 import com.android.car.ui.core.CarUi;
52 import com.android.car.ui.toolbar.MenuItem;
53 import com.android.car.ui.toolbar.ToolbarController;
54 
55 import java.util.List;
56 
57 /**
58  * Main activity for the Dialer app. It hosts most of the fragments for the app.
59  *
60  * <p>Start {@link InCallActivity} if there are ongoing calls
61  *
62  * <p>Based on call and connectivity status, it will choose the right page to display.
63  */
64 public class TelecomActivity extends FragmentActivity implements
65         DialerBaseFragment.DialerFragmentParent, InsetsChangedListener {
66     private static final String TAG = "CD.TelecomActivity";
67     private LiveData<String> mBluetoothErrorMsgLiveData;
68     private LiveData<List<Call>> mOngoingCallListLiveData;
69     // View objects for this activity.
70     private TelecomPageTab.Factory mTabFactory;
71     private BluetoothDevice mBluetoothDevice;
72     private ToolbarController mCarUiToolbar;
73 
74     @Override
onCreate(Bundle savedInstanceState)75     protected void onCreate(Bundle savedInstanceState) {
76         super.onCreate(savedInstanceState);
77 
78         L.d(TAG, "onCreate");
79 
80         setContentView(R.layout.telecom_activity);
81 
82         mCarUiToolbar = CarUi.requireToolbar(this);
83 
84         setupTabLayout();
85 
86         TelecomActivityViewModel viewModel = ViewModelProviders.of(this).get(
87                 TelecomActivityViewModel.class);
88         mBluetoothErrorMsgLiveData = viewModel.getErrorMessage();
89         mBluetoothErrorMsgLiveData.observe(this, (String error) -> {
90             if (!BluetoothErrorStringLiveData.NO_BT_ERROR.equals(error)) {
91                 startActivity(new Intent(this, NoHfpActivity.class));
92                 finish();
93             }
94         });
95 
96         MutableLiveData<Integer> toolbarTitleMode = viewModel.getToolbarTitleMode();
97         toolbarTitleMode.setValue(Themes.getAttrInteger(this, R.attr.toolbarTitleMode));
98         viewModel.getRefreshTabsLiveData().observe(this, this::refreshTabs);
99 
100         InCallViewModel inCallViewModel = ViewModelProviders.of(this).get(InCallViewModel.class);
101         mOngoingCallListLiveData = inCallViewModel.getOngoingCallList();
102         // The mOngoingCallListLiveData needs to be active to get calculated.
103         mOngoingCallListLiveData.observe(this, this::maybeStartInCallActivity);
104 
105         handleIntent();
106     }
107 
refreshTabs(boolean refreshTabs)108     private void refreshTabs(boolean refreshTabs) {
109         L.v(TAG, "hfp connected device list Changes.");
110         if (refreshTabs) {
111             setupTabLayout();
112         }
113     }
114 
115     @Override
onNewIntent(Intent i)116     protected void onNewIntent(Intent i) {
117         super.onNewIntent(i);
118         setIntent(i);
119         handleIntent();
120     }
121 
handleIntent()122     private void handleIntent() {
123         Intent intent = getIntent();
124         String action = intent != null ? intent.getAction() : null;
125         L.d(TAG, "handleIntent, intent: %s, action: %s", intent, action);
126         if (action == null || action.length() == 0) {
127             return;
128         }
129 
130         String number;
131         switch (action) {
132             case Intent.ACTION_DIAL:
133                 number = PhoneNumberUtils.getNumberFromIntent(intent, this);
134                 showDialPadFragment(number);
135                 break;
136 
137             case Intent.ACTION_CALL:
138                 number = PhoneNumberUtils.getNumberFromIntent(intent, this);
139                 UiCallManager.get().placeCall(number);
140                 break;
141 
142             case Intent.ACTION_SEARCH:
143                 String searchQuery = intent.getStringExtra(SearchManager.QUERY);
144                 navigateToContactResultsFragment(searchQuery);
145                 break;
146 
147             case Constants.Intents.ACTION_SHOW_PAGE:
148                 showTabPage(intent.getStringExtra(Constants.Intents.EXTRA_SHOW_PAGE));
149                 if (intent.getBooleanExtra(Constants.Intents.EXTRA_ACTION_READ_MISSED, false)) {
150                     NotificationService.readAllMissedCall(this);
151                 }
152                 break;
153             case Intent.ACTION_VIEW:
154                 if (CallLog.Calls.CONTENT_TYPE.equals(intent.getType())) {
155                     showTabPage(TelecomPageTab.Page.CALL_HISTORY);
156                 }
157                 break;
158             default:
159                 // Do nothing.
160         }
161 
162         setIntent(null);
163 
164         // This is to start the incall activity when user taps on the dialer launch icon rapidly
165         maybeStartInCallActivity(mOngoingCallListLiveData.getValue());
166     }
167 
setupTabLayout()168     private void setupTabLayout() {
169         boolean wasContentFragmentRestored = false;
170         mTabFactory = new TelecomPageTab.Factory(this, getSupportFragmentManager());
171         mCarUiToolbar.clearAllTabs();
172         for (int i = 0; i < mTabFactory.getTabCount(); i++) {
173             TelecomPageTab tab = mTabFactory.createTab(getBaseContext(), i);
174             mCarUiToolbar.addTab(tab);
175 
176             if (tab.wasFragmentRestored()) {
177                 mCarUiToolbar.selectTab(i);
178                 wasContentFragmentRestored = true;
179             }
180         }
181 
182         // Select the starting tab and set up the fragment for it.
183         if (!wasContentFragmentRestored) {
184             int startTabIndex = getTabFromSharedPreference();
185             TelecomPageTab startTab = (TelecomPageTab) mCarUiToolbar.getTab(startTabIndex);
186             mCarUiToolbar.selectTab(startTabIndex);
187             setContentFragment(startTab.getFragment(), startTab.getFragmentTag());
188         }
189 
190         mCarUiToolbar.registerOnTabSelectedListener(
191                 tab -> {
192                     TelecomPageTab telecomPageTab = (TelecomPageTab) tab;
193                     Fragment fragment = telecomPageTab.getFragment();
194                     setContentFragment(fragment, telecomPageTab.getFragmentTag());
195                 });
196     }
197 
198     /**
199      * Switch to {@link DialpadFragment} and set the given number as dialed number.
200      */
showDialPadFragment(String number)201     private void showDialPadFragment(String number) {
202         int dialpadTabIndex = showTabPage(TelecomPageTab.Page.DIAL_PAD);
203 
204         if (dialpadTabIndex == -1) {
205             return;
206         }
207 
208         TelecomPageTab dialpadTab = (TelecomPageTab) mCarUiToolbar.getTab(dialpadTabIndex);
209         Fragment fragment = dialpadTab.getFragment();
210         if (fragment instanceof DialpadFragment) {
211             ((DialpadFragment) fragment).setDialedNumber(number);
212         } else {
213             L.w(TAG, "Current tab is not a dialpad fragment!");
214         }
215     }
216 
showTabPage(@elecomPageTab.Page String tabPage)217     private int showTabPage(@TelecomPageTab.Page String tabPage) {
218         int tabIndex = mTabFactory.getTabIndex(tabPage);
219         if (tabIndex == -1) {
220             L.w(TAG, "Page %s is not a tab.", tabPage);
221             return -1;
222         }
223         getSupportFragmentManager().executePendingTransactions();
224         while (isBackNavigationAvailable()) {
225             getSupportFragmentManager().popBackStackImmediate();
226         }
227 
228         mCarUiToolbar.selectTab(tabIndex);
229         return tabIndex;
230     }
231 
setContentFragment(Fragment fragment, String fragmentTag)232     private void setContentFragment(Fragment fragment, String fragmentTag) {
233         L.d(TAG, "setContentFragment: %s", fragment);
234 
235         getSupportFragmentManager().executePendingTransactions();
236         while (getSupportFragmentManager().getBackStackEntryCount() > 0) {
237             getSupportFragmentManager().popBackStackImmediate();
238         }
239 
240         getSupportFragmentManager()
241                 .beginTransaction()
242                 .replace(R.id.fragment_container, fragment, fragmentTag)
243                 .addToBackStack(fragmentTag)
244                 .commit();
245     }
246 
247     @Override
pushContentFragment(@onNull Fragment topContentFragment, String fragmentTag)248     public void pushContentFragment(@NonNull Fragment topContentFragment, String fragmentTag) {
249         L.d(TAG, "pushContentFragment: %s", topContentFragment);
250 
251         getSupportFragmentManager()
252                 .beginTransaction()
253                 .replace(R.id.fragment_container, topContentFragment, fragmentTag)
254                 .addToBackStack(fragmentTag)
255                 .commit();
256     }
257 
258     @Override
onNavigateUp()259     public boolean onNavigateUp() {
260         if (isBackNavigationAvailable()) {
261             onBackPressed();
262             return true;
263         }
264         return super.onNavigateUp();
265     }
266 
267     @Override
onBackPressed()268     public void onBackPressed() {
269         // By default onBackPressed will pop all the fragments off the backstack and then finish
270         // the activity. We want to finish the activity while there is still one fragment on the
271         // backstack, because we use onBackStackChanged() to set up our fragments.
272         if (isBackNavigationAvailable()) {
273             super.onBackPressed();
274         } else {
275             finishAfterTransition();
276         }
277     }
278 
279     /**
280      * Handles the click action on the menu items.
281      */
onMenuItemClicked(MenuItem item)282     public void onMenuItemClicked(MenuItem item) {
283         switch (item.getId()) {
284             case R.id.menu_item_search:
285                 Intent searchIntent = new Intent(getApplicationContext(), TelecomActivity.class);
286                 searchIntent.setAction(Intent.ACTION_SEARCH);
287                 startActivity(searchIntent);
288                 break;
289             case R.id.menu_item_setting:
290                 Intent settingsIntent = new Intent(getApplicationContext(),
291                         DialerSettingsActivity.class);
292                 startActivity(settingsIntent);
293                 break;
294         }
295     }
296 
navigateToContactResultsFragment(String query)297     private void navigateToContactResultsFragment(String query) {
298         Fragment topFragment = getSupportFragmentManager().findFragmentById(
299                 R.id.fragment_container);
300 
301         // Top fragment is ContactResultsFragment, update search query
302         if (topFragment instanceof ContactResultsFragment) {
303             ((ContactResultsFragment) topFragment).setSearchQuery(query);
304             return;
305         }
306 
307         ContactResultsFragment fragment = ContactResultsFragment.newInstance(query);
308         pushContentFragment(fragment, ContactResultsFragment.FRAGMENT_TAG);
309     }
310 
maybeStartInCallActivity(List<Call> callList)311     private void maybeStartInCallActivity(List<Call> callList) {
312         if (callList == null || callList.isEmpty()) {
313             return;
314         }
315 
316         L.d(TAG, "Start InCallActivity");
317         Intent launchIntent = new Intent(getApplicationContext(), InCallActivity.class);
318         startActivity(launchIntent);
319     }
320 
321     /**
322      * If the back button on action bar is available to navigate up.
323      */
isBackNavigationAvailable()324     private boolean isBackNavigationAvailable() {
325         return getSupportFragmentManager().getBackStackEntryCount() > 1;
326     }
327 
getTabFromSharedPreference()328     private int getTabFromSharedPreference() {
329         String key = getResources().getString(R.string.pref_start_page_key);
330         String defaultValue = getResources().getStringArray(R.array.tabs_config)[0];
331         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
332         return mTabFactory.getTabIndex(sharedPreferences.getString(key, defaultValue));
333     }
334 
335     @Override
onCarUiInsetsChanged(Insets insets)336     public void onCarUiInsetsChanged(Insets insets) {
337         // Do nothing, this is just a marker that we will handle the insets in fragments.
338         // This is only necessary because the fragments are not immediately added to the
339         // activity when calling .commit()
340     }
341 }
342