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