• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.dialer.searchfragment.list;
18 
19 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
20 
21 import android.app.Fragment;
22 import android.app.LoaderManager.LoaderCallbacks;
23 import android.content.Intent;
24 import android.content.Loader;
25 import android.content.pm.PackageManager;
26 import android.database.Cursor;
27 import android.os.Bundle;
28 import android.preference.PreferenceManager;
29 import android.support.annotation.NonNull;
30 import android.support.annotation.Nullable;
31 import android.support.annotation.VisibleForTesting;
32 import android.support.v7.widget.LinearLayoutManager;
33 import android.support.v7.widget.RecyclerView;
34 import android.telephony.PhoneNumberUtils;
35 import android.text.TextUtils;
36 import android.view.LayoutInflater;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.View.OnTouchListener;
40 import android.view.ViewGroup;
41 import android.view.animation.Interpolator;
42 import android.widget.FrameLayout;
43 import android.widget.FrameLayout.LayoutParams;
44 import com.android.contacts.common.extensions.PhoneDirectoryExtenderAccessor;
45 import com.android.dialer.animation.AnimUtils;
46 import com.android.dialer.callcomposer.CallComposerActivity;
47 import com.android.dialer.callintent.CallInitiationType;
48 import com.android.dialer.callintent.CallIntentBuilder;
49 import com.android.dialer.callintent.CallSpecificAppData;
50 import com.android.dialer.common.Assert;
51 import com.android.dialer.common.FragmentUtils;
52 import com.android.dialer.common.LogUtil;
53 import com.android.dialer.common.concurrent.ThreadUtil;
54 import com.android.dialer.dialercontact.DialerContact;
55 import com.android.dialer.enrichedcall.EnrichedCallComponent;
56 import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
57 import com.android.dialer.logging.DialerImpression;
58 import com.android.dialer.logging.Logger;
59 import com.android.dialer.precall.PreCall;
60 import com.android.dialer.searchfragment.common.RowClickListener;
61 import com.android.dialer.searchfragment.common.SearchCursor;
62 import com.android.dialer.searchfragment.cp2.SearchContactsCursorLoader;
63 import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader;
64 import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Directory;
65 import com.android.dialer.searchfragment.directories.DirectoryContactsCursorLoader;
66 import com.android.dialer.searchfragment.list.SearchActionViewHolder.Action;
67 import com.android.dialer.searchfragment.nearbyplaces.NearbyPlacesCursorLoader;
68 import com.android.dialer.util.CallUtil;
69 import com.android.dialer.util.DialerUtils;
70 import com.android.dialer.util.PermissionsUtil;
71 import com.android.dialer.util.ViewUtil;
72 import com.android.dialer.widget.EmptyContentView;
73 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
74 import java.util.ArrayList;
75 import java.util.Arrays;
76 import java.util.Collections;
77 import java.util.List;
78 
79 /** Fragment used for searching contacts. */
80 public final class NewSearchFragment extends Fragment
81     implements LoaderCallbacks<Cursor>,
82         OnEmptyViewActionButtonClickedListener,
83         CapabilitiesListener,
84         OnTouchListener,
85         RowClickListener {
86 
87   // Since some of our queries can generate network requests, we should delay them until the user
88   // stops typing to prevent generating too much network traffic.
89   private static final int NETWORK_SEARCH_DELAY_MILLIS = 300;
90   // To prevent constant capabilities updates refreshing the adapter, we want to add a delay between
91   // updates so they are bundled together
92   private static final int ENRICHED_CALLING_CAPABILITIES_UPDATED_DELAY = 400;
93 
94   private static final String KEY_LOCATION_PROMPT_DISMISSED = "search_location_prompt_dismissed";
95 
96   @VisibleForTesting public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
97   @VisibleForTesting private static final int LOCATION_PERMISSION_REQUEST_CODE = 2;
98 
99   private static final int CONTACTS_LOADER_ID = 0;
100   private static final int NEARBY_PLACES_LOADER_ID = 1;
101 
102   // ID for the loader that loads info about all directories (local & remote).
103   private static final int DIRECTORIES_LOADER_ID = 2;
104 
105   private static final int DIRECTORY_CONTACTS_LOADER_ID = 3;
106 
107   private static final String KEY_QUERY = "key_query";
108   private static final String KEY_CALL_INITIATION_TYPE = "key_call_initiation_type";
109 
110   private EmptyContentView emptyContentView;
111   private RecyclerView recyclerView;
112   private SearchAdapter adapter;
113   private String query;
114   // Raw query number from dialpad, which may contain special character such as "+". This is used
115   // for actions to add contact or send sms.
116   private String rawNumber;
117   private CallInitiationType.Type callInitiationType = CallInitiationType.Type.UNKNOWN_INITIATION;
118   private boolean directoriesDisabledForTesting;
119 
120   // Information about all local & remote directories (including ID, display name, etc, but not
121   // the contacts in them).
122   private final List<Directory> directories = new ArrayList<>();
123   private final Runnable loaderCp2ContactsRunnable =
124       () -> {
125         if (getHost() != null) {
126           getLoaderManager().restartLoader(CONTACTS_LOADER_ID, null, this);
127         }
128       };
129   private final Runnable loadNearbyPlacesRunnable =
130       () -> {
131         if (getHost() != null) {
132           getLoaderManager().restartLoader(NEARBY_PLACES_LOADER_ID, null, this);
133         }
134       };
135   private final Runnable loadDirectoryContactsRunnable =
136       () -> {
137         if (getHost() != null) {
138           getLoaderManager().restartLoader(DIRECTORY_CONTACTS_LOADER_ID, null, this);
139         }
140       };
141   private final Runnable capabilitiesUpdatedRunnable = () -> adapter.notifyDataSetChanged();
142 
143   private Runnable updatePositionRunnable;
144 
newInstance()145   public static NewSearchFragment newInstance() {
146     return new NewSearchFragment();
147   }
148 
149   @Nullable
150   @Override
onCreateView( LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState)151   public View onCreateView(
152       LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) {
153     View view = inflater.inflate(R.layout.fragment_search, parent, false);
154     adapter = new SearchAdapter(getContext(), new SearchCursorManager(), this);
155     adapter.setQuery(query, rawNumber);
156     adapter.setSearchActions(getActions());
157     showLocationPermission();
158     emptyContentView = view.findViewById(R.id.empty_view);
159     recyclerView = view.findViewById(R.id.recycler_view);
160     recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
161     recyclerView.setOnTouchListener(this);
162     recyclerView.setAdapter(adapter);
163 
164     if (!PermissionsUtil.hasContactsReadPermissions(getContext())) {
165       emptyContentView.setDescription(R.string.new_permission_no_search);
166       emptyContentView.setActionLabel(R.string.permission_single_turn_on);
167       emptyContentView.setActionClickedListener(this);
168       emptyContentView.setImage(R.drawable.empty_contacts);
169       emptyContentView.setVisibility(View.VISIBLE);
170     } else {
171       initLoaders();
172     }
173 
174     if (savedInstanceState != null) {
175       setQuery(
176           savedInstanceState.getString(KEY_QUERY),
177           CallInitiationType.Type.forNumber(savedInstanceState.getInt(KEY_CALL_INITIATION_TYPE)));
178     }
179 
180     if (updatePositionRunnable != null) {
181       ViewUtil.doOnPreDraw(view, false, updatePositionRunnable);
182     }
183     return view;
184   }
185 
186   @Override
onSaveInstanceState(Bundle outState)187   public void onSaveInstanceState(Bundle outState) {
188     super.onSaveInstanceState(outState);
189     outState.putInt(KEY_CALL_INITIATION_TYPE, callInitiationType.getNumber());
190     outState.putString(KEY_QUERY, query);
191   }
192 
initLoaders()193   private void initLoaders() {
194     getLoaderManager().initLoader(CONTACTS_LOADER_ID, null, this);
195     loadDirectoriesCursor();
196   }
197 
198   @Override
onCreateLoader(int id, Bundle bundle)199   public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
200     LogUtil.i("NewSearchFragment.onCreateLoader", "loading cursor: " + id);
201     if (id == CONTACTS_LOADER_ID) {
202       return new SearchContactsCursorLoader(getContext(), query, isRegularSearch());
203     } else if (id == NEARBY_PLACES_LOADER_ID) {
204       // Directories represent contact data sources on the device, but since nearby places aren't
205       // stored on the device, they don't have a directory ID. We pass the list of all existing IDs
206       // so that we can find one that doesn't collide.
207       List<Long> directoryIds = new ArrayList<>();
208       for (Directory directory : directories) {
209         directoryIds.add(directory.getId());
210       }
211       return new NearbyPlacesCursorLoader(getContext(), query, directoryIds);
212     } else if (id == DIRECTORIES_LOADER_ID) {
213       return new DirectoriesCursorLoader(getContext());
214     } else if (id == DIRECTORY_CONTACTS_LOADER_ID) {
215       return new DirectoryContactsCursorLoader(getContext(), query, directories);
216     } else {
217       throw new IllegalStateException("Invalid loader id: " + id);
218     }
219   }
220 
221   @Override
onLoadFinished(Loader<Cursor> loader, Cursor cursor)222   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
223     LogUtil.i("NewSearchFragment.onLoadFinished", "Loader finished: " + loader);
224     if (cursor != null
225         && !(loader instanceof DirectoriesCursorLoader)
226         && !(cursor instanceof SearchCursor)) {
227       throw Assert.createIllegalStateFailException("Cursors must implement SearchCursor");
228     }
229 
230     if (loader instanceof SearchContactsCursorLoader) {
231       adapter.setContactsCursor((SearchCursor) cursor);
232 
233     } else if (loader instanceof NearbyPlacesCursorLoader) {
234       adapter.setNearbyPlacesCursor((SearchCursor) cursor);
235 
236     } else if (loader instanceof DirectoryContactsCursorLoader) {
237       adapter.setDirectoryContactsCursor((SearchCursor) cursor);
238 
239     } else if (loader instanceof DirectoriesCursorLoader) {
240       directories.clear();
241       directories.addAll(DirectoriesCursorLoader.toDirectories(cursor));
242       loadNearbyPlacesCursor();
243       loadDirectoryContactsCursors();
244 
245     } else {
246       throw new IllegalStateException("Invalid loader: " + loader);
247     }
248   }
249 
250   @Override
onLoaderReset(Loader<Cursor> loader)251   public void onLoaderReset(Loader<Cursor> loader) {
252     LogUtil.i("NewSearchFragment.onLoaderReset", "Loader reset: " + loader);
253     if (loader instanceof SearchContactsCursorLoader) {
254       adapter.setContactsCursor(null);
255     } else if (loader instanceof NearbyPlacesCursorLoader) {
256       adapter.setNearbyPlacesCursor(null);
257     } else if (loader instanceof DirectoryContactsCursorLoader) {
258       adapter.setDirectoryContactsCursor(null);
259     }
260   }
261 
setRawNumber(String rawNumber)262   public void setRawNumber(String rawNumber) {
263     this.rawNumber = rawNumber;
264   }
265 
266   @VisibleForTesting
getRawNumber()267   public String getRawNumber() {
268     return rawNumber;
269   }
270 
setQuery(String query, CallInitiationType.Type callInitiationType)271   public void setQuery(String query, CallInitiationType.Type callInitiationType) {
272     this.query = query;
273     this.callInitiationType = callInitiationType;
274     if (adapter != null) {
275       adapter.setQuery(query, rawNumber);
276       adapter.setSearchActions(getActions());
277       showLocationPermission();
278       loadCp2ContactsCursor();
279       loadNearbyPlacesCursor();
280       loadDirectoryContactsCursors();
281     }
282   }
283 
284   /** Returns true if the location permission was shown. */
showLocationPermission()285   private boolean showLocationPermission() {
286     if (adapter == null) {
287       return false;
288     }
289 
290     if (getContext() == null
291         || PermissionsUtil.hasLocationPermissions(getContext())
292         || hasBeenDismissed()
293         || !isRegularSearch()) {
294       adapter.hideLocationPermissionRequest();
295       return false;
296     }
297 
298     adapter.showLocationPermissionRequest(
299         v -> requestLocationPermission(), v -> dismissLocationPermission());
300     return true;
301   }
302 
303   /** Translate the search fragment and resize it to fit on the screen. */
animatePosition(int start, int end, int duration)304   public void animatePosition(int start, int end, int duration) {
305     // Called before the view is ready, prepare a runnable to run in onCreateView
306     if (getView() == null) {
307       updatePositionRunnable = () -> animatePosition(start, end, 0);
308       return;
309     }
310     boolean slideUp = start > end;
311     Interpolator interpolator = slideUp ? AnimUtils.EASE_IN : AnimUtils.EASE_OUT;
312     int startHeight = getActivity().findViewById(android.R.id.content).getHeight();
313     int endHeight = startHeight - (end - start);
314     getView().setTranslationY(start);
315     getView()
316         .animate()
317         .translationY(end)
318         .setInterpolator(interpolator)
319         .setDuration(duration)
320         .setUpdateListener(
321             animation -> setHeight(startHeight, endHeight, animation.getAnimatedFraction()));
322     updatePositionRunnable = null;
323   }
324 
setHeight(int start, int end, float percentage)325   private void setHeight(int start, int end, float percentage) {
326     View view = getView();
327     if (view == null) {
328       return;
329     }
330 
331     FrameLayout.LayoutParams params = (LayoutParams) view.getLayoutParams();
332     params.height = (int) (start + (end - start) * percentage);
333     view.setLayoutParams(params);
334   }
335 
336   @Override
onDestroy()337   public void onDestroy() {
338     super.onDestroy();
339     ThreadUtil.getUiThreadHandler().removeCallbacks(loaderCp2ContactsRunnable);
340     ThreadUtil.getUiThreadHandler().removeCallbacks(loadNearbyPlacesRunnable);
341     ThreadUtil.getUiThreadHandler().removeCallbacks(loadDirectoryContactsRunnable);
342     ThreadUtil.getUiThreadHandler().removeCallbacks(capabilitiesUpdatedRunnable);
343   }
344 
345   @Override
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)346   public void onRequestPermissionsResult(
347       int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
348     if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
349       if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
350         // Force a refresh of the data since we were missing the permission before this.
351         emptyContentView.setVisibility(View.GONE);
352         initLoaders();
353       }
354     } else if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
355       if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
356         // Force a refresh of the data since we were missing the permission before this.
357         loadNearbyPlacesCursor();
358         adapter.hideLocationPermissionRequest();
359       }
360     }
361   }
362 
363   @Override
onEmptyViewActionButtonClicked()364   public void onEmptyViewActionButtonClicked() {
365     String[] deniedPermissions =
366         PermissionsUtil.getPermissionsCurrentlyDenied(
367             getContext(), PermissionsUtil.allContactsGroupPermissionsUsedInDialer);
368     if (deniedPermissions.length > 0) {
369       LogUtil.i(
370           "NewSearchFragment.onEmptyViewActionButtonClicked",
371           "Requesting permissions: " + Arrays.toString(deniedPermissions));
372       FragmentUtils.getParentUnsafe(this, SearchFragmentListener.class).requestingPermission();
373       requestPermissions(deniedPermissions, READ_CONTACTS_PERMISSION_REQUEST_CODE);
374     }
375   }
376 
377   /** Loads info about all directories (local & remote). */
loadDirectoriesCursor()378   private void loadDirectoriesCursor() {
379     if (!directoriesDisabledForTesting) {
380       getLoaderManager().initLoader(DIRECTORIES_LOADER_ID, null, this);
381     }
382   }
383 
384   /**
385    * Loads contacts stored in directories.
386    *
387    * <p>Should not be called before finishing loading info about all directories (local & remote).
388    */
loadDirectoryContactsCursors()389   private void loadDirectoryContactsCursors() {
390     if (directoriesDisabledForTesting) {
391       return;
392     }
393 
394     // Cancel existing load if one exists.
395     ThreadUtil.getUiThreadHandler().removeCallbacks(loadDirectoryContactsRunnable);
396     ThreadUtil.getUiThreadHandler()
397         .postDelayed(loadDirectoryContactsRunnable, NETWORK_SEARCH_DELAY_MILLIS);
398   }
399 
loadCp2ContactsCursor()400   private void loadCp2ContactsCursor() {
401     // Cancel existing load if one exists.
402     ThreadUtil.getUiThreadHandler().removeCallbacks(loaderCp2ContactsRunnable);
403     ThreadUtil.getUiThreadHandler()
404         .postDelayed(loaderCp2ContactsRunnable, NETWORK_SEARCH_DELAY_MILLIS);
405   }
406 
407   /**
408    * Loads nearby places.
409    *
410    * <p>Should not be called before finishing loading info about all directories (local and remote).
411    */
loadNearbyPlacesCursor()412   private void loadNearbyPlacesCursor() {
413     // If we're requesting the location permission, don't load nearby places cursor.
414     if (showLocationPermission()) {
415       return;
416     }
417 
418     // If the user dismissed the prompt without granting us the permission, don't load the cursor.
419     if (getContext() == null || !PermissionsUtil.hasLocationPermissions(getContext())) {
420       return;
421     }
422 
423     // Cancel existing load if one exists.
424     ThreadUtil.getUiThreadHandler().removeCallbacks(loadNearbyPlacesRunnable);
425 
426     // If nearby places is not enabled, do not try to load them.
427     if (!PhoneDirectoryExtenderAccessor.get(getContext()).isEnabled(getContext())) {
428       return;
429     }
430     ThreadUtil.getUiThreadHandler()
431         .postDelayed(loadNearbyPlacesRunnable, NETWORK_SEARCH_DELAY_MILLIS);
432   }
433 
requestLocationPermission()434   private void requestLocationPermission() {
435     Assert.checkArgument(
436         !PermissionsUtil.hasPermission(getContext(), ACCESS_FINE_LOCATION),
437         "attempted to request already granted location permission");
438     String[] deniedPermissions =
439         PermissionsUtil.getPermissionsCurrentlyDenied(
440             getContext(), PermissionsUtil.allLocationGroupPermissionsUsedInDialer);
441     FragmentUtils.getParentUnsafe(this, SearchFragmentListener.class).requestingPermission();
442     requestPermissions(deniedPermissions, LOCATION_PERMISSION_REQUEST_CODE);
443   }
444 
445   @VisibleForTesting
dismissLocationPermission()446   public void dismissLocationPermission() {
447     PreferenceManager.getDefaultSharedPreferences(getContext())
448         .edit()
449         .putBoolean(KEY_LOCATION_PROMPT_DISMISSED, true)
450         .apply();
451     adapter.hideLocationPermissionRequest();
452   }
453 
hasBeenDismissed()454   private boolean hasBeenDismissed() {
455     return PreferenceManager.getDefaultSharedPreferences(getContext())
456         .getBoolean(KEY_LOCATION_PROMPT_DISMISSED, false);
457   }
458 
459   @Override
onResume()460   public void onResume() {
461     super.onResume();
462     EnrichedCallComponent.get(getContext())
463         .getEnrichedCallManager()
464         .registerCapabilitiesListener(this);
465     getLoaderManager().restartLoader(CONTACTS_LOADER_ID, null, this);
466   }
467 
468   @Override
onPause()469   public void onPause() {
470     super.onPause();
471     EnrichedCallComponent.get(getContext())
472         .getEnrichedCallManager()
473         .unregisterCapabilitiesListener(this);
474   }
475 
476   @Override
onCapabilitiesUpdated()477   public void onCapabilitiesUpdated() {
478     ThreadUtil.getUiThreadHandler().removeCallbacks(capabilitiesUpdatedRunnable);
479     ThreadUtil.getUiThreadHandler()
480         .postDelayed(capabilitiesUpdatedRunnable, ENRICHED_CALLING_CAPABILITIES_UPDATED_DELAY);
481   }
482 
483   // Currently, setting up multiple FakeContentProviders doesn't work and results in this fragment
484   // being untestable while it can query multiple datasources. This is a temporary fix.
485   // TODO(a bug): Remove this method and test this fragment with multiple data sources
486   @VisibleForTesting
setDirectoriesDisabled(boolean disabled)487   public void setDirectoriesDisabled(boolean disabled) {
488     directoriesDisabledForTesting = disabled;
489   }
490 
491   /**
492    * Returns a list of search actions to be shown in the search results.
493    *
494    * <p>List will be empty if query is 1 or 0 characters or the query isn't from the Dialpad. For
495    * the list of supported actions, see {@link SearchActionViewHolder.Action}.
496    */
getActions()497   private List<Integer> getActions() {
498     boolean isDialableNumber = PhoneNumberUtils.isGlobalPhoneNumber(query);
499     boolean nonDialableQueryInRegularSearch = isRegularSearch() && !isDialableNumber;
500     if (TextUtils.isEmpty(query) || query.length() == 1 || nonDialableQueryInRegularSearch) {
501       return Collections.emptyList();
502     }
503 
504     List<Integer> actions = new ArrayList<>();
505     if (!isRegularSearch()) {
506       actions.add(Action.CREATE_NEW_CONTACT);
507       actions.add(Action.ADD_TO_CONTACT);
508     }
509 
510     if (isRegularSearch() && isDialableNumber) {
511       actions.add(Action.MAKE_VOICE_CALL);
512     }
513 
514     actions.add(Action.SEND_SMS);
515     if (CallUtil.isVideoEnabled(getContext())) {
516       actions.add(Action.MAKE_VILTE_CALL);
517     }
518 
519     return actions;
520   }
521 
522   // Returns true if currently in Regular Search (as opposed to Dialpad Search).
isRegularSearch()523   private boolean isRegularSearch() {
524     return callInitiationType == CallInitiationType.Type.REGULAR_SEARCH;
525   }
526 
527   @Override
onTouch(View v, MotionEvent event)528   public boolean onTouch(View v, MotionEvent event) {
529     if (event.getAction() == MotionEvent.ACTION_UP) {
530       v.performClick();
531     }
532     if (event.getAction() == MotionEvent.ACTION_DOWN) {
533       FragmentUtils.getParentUnsafe(this, SearchFragmentListener.class).onSearchListTouch();
534     }
535     return false;
536   }
537 
538   @Override
placeVoiceCall(String phoneNumber, int ranking)539   public void placeVoiceCall(String phoneNumber, int ranking) {
540     placeCall(phoneNumber, ranking, false);
541   }
542 
543   @Override
placeVideoCall(String phoneNumber, int ranking)544   public void placeVideoCall(String phoneNumber, int ranking) {
545     placeCall(phoneNumber, ranking, true);
546   }
547 
placeCall(String phoneNumber, int position, boolean isVideoCall)548   private void placeCall(String phoneNumber, int position, boolean isVideoCall) {
549     CallSpecificAppData callSpecificAppData =
550         CallSpecificAppData.newBuilder()
551             .setCallInitiationType(callInitiationType)
552             .setPositionOfSelectedSearchResult(position)
553             .setCharactersInSearchString(query == null ? 0 : query.length())
554             .setAllowAssistedDialing(true)
555             .build();
556     PreCall.start(
557         getContext(),
558         new CallIntentBuilder(phoneNumber, callSpecificAppData)
559             .setIsVideoCall(isVideoCall)
560             .setAllowAssistedDial(true));
561     FragmentUtils.getParentUnsafe(this, SearchFragmentListener.class).onCallPlacedFromSearch();
562   }
563 
564   @Override
placeDuoCall(String phoneNumber)565   public void placeDuoCall(String phoneNumber) {
566     Logger.get(getContext())
567         .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_SEARCH);
568     PreCall.start(
569         getContext(),
570         new CallIntentBuilder(phoneNumber, CallInitiationType.Type.REGULAR_SEARCH)
571             .setIsVideoCall(true)
572             .setIsDuoCall(true));
573     FragmentUtils.getParentUnsafe(this, SearchFragmentListener.class).onCallPlacedFromSearch();
574   }
575 
576   @Override
openCallAndShare(DialerContact contact)577   public void openCallAndShare(DialerContact contact) {
578     Intent intent = CallComposerActivity.newIntent(getContext(), contact);
579     DialerUtils.startActivityWithErrorToast(getContext(), intent);
580   }
581 
582   /** Callback to {@link NewSearchFragment}'s parent to be notified of important events. */
583   public interface SearchFragmentListener {
584 
585     /** Called when the list view in {@link NewSearchFragment} is clicked. */
onSearchListTouch()586     void onSearchListTouch();
587 
588     /** Called when a call is placed from the search fragment. */
onCallPlacedFromSearch()589     void onCallPlacedFromSearch();
590 
591     /** Called when a permission is about to be requested. */
requestingPermission()592     void requestingPermission();
593   }
594 }
595