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