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