1 /* 2 * Copyright (C) 2010 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 package com.android.contacts.common.list; 17 18 import android.content.ComponentName; 19 import android.content.Intent; 20 import android.content.Loader; 21 import android.database.Cursor; 22 import android.os.Bundle; 23 import android.support.annotation.MainThread; 24 import android.support.annotation.Nullable; 25 import android.text.TextUtils; 26 import android.util.ArraySet; 27 import android.view.LayoutInflater; 28 import android.view.MenuItem; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import com.android.contacts.common.R; 32 import com.android.contacts.common.util.AccountFilterUtil; 33 import com.android.dialer.callcomposer.CallComposerContact; 34 import com.android.dialer.callintent.CallInitiationType; 35 import com.android.dialer.callintent.CallInitiationType.Type; 36 import com.android.dialer.callintent.CallSpecificAppData; 37 import com.android.dialer.common.Assert; 38 import com.android.dialer.common.LogUtil; 39 import com.android.dialer.enrichedcall.EnrichedCallComponent; 40 import com.android.dialer.enrichedcall.EnrichedCallManager; 41 import com.android.dialer.logging.Logger; 42 import com.android.dialer.protos.ProtoParsers; 43 import java.util.Set; 44 import org.json.JSONException; 45 import org.json.JSONObject; 46 47 /** Fragment containing a phone number list for picking. */ 48 public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter> 49 implements PhoneNumberListAdapter.Listener, EnrichedCallManager.CapabilitiesListener { 50 51 private static final String KEY_FILTER = "filter"; 52 private OnPhoneNumberPickerActionListener mListener; 53 private ContactListFilter mFilter; 54 private View mAccountFilterHeader; 55 /** 56 * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set to View.GONE. 57 */ 58 private View mPaddingView; 59 /** true if the loader has started at least once. */ 60 private boolean mLoaderStarted; 61 62 private boolean mUseCallableUri; 63 64 private ContactListItemView.PhotoPosition mPhotoPosition = 65 ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */); 66 67 private final Set<OnLoadFinishedListener> mLoadFinishedListeners = new ArraySet<>(); 68 69 private CursorReranker mCursorReranker; 70 PhoneNumberPickerFragment()71 public PhoneNumberPickerFragment() { 72 setQuickContactEnabled(false); 73 setPhotoLoaderEnabled(true); 74 setSectionHeaderDisplayEnabled(true); 75 setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); 76 77 // Show nothing instead of letting caller Activity show something. 78 setHasOptionsMenu(true); 79 } 80 81 /** 82 * Handles a click on the video call icon for a row in the list. 83 * 84 * @param position The position in the list where the click ocurred. 85 */ 86 @Override onVideoCallIconClicked(int position)87 public void onVideoCallIconClicked(int position) { 88 callNumber(position, true /* isVideoCall */); 89 } 90 91 @Override onCallAndShareIconClicked(int position)92 public void onCallAndShareIconClicked(int position) { 93 // Required because of cyclic dependencies of everything depending on contacts/common. 94 String componentName = "com.android.dialer.callcomposer.CallComposerActivity"; 95 Intent intent = new Intent(); 96 intent.setComponent(new ComponentName(getContext(), componentName)); 97 CallComposerContact contact = 98 ((PhoneNumberListAdapter) getAdapter()).getCallComposerContact(position); 99 ProtoParsers.put(intent, "CALL_COMPOSER_CONTACT", contact); 100 startActivity(intent); 101 } 102 setDirectorySearchEnabled(boolean flag)103 public void setDirectorySearchEnabled(boolean flag) { 104 setDirectorySearchMode( 105 flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : DirectoryListLoader.SEARCH_MODE_NONE); 106 } 107 setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener)108 public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) { 109 this.mListener = listener; 110 } 111 getOnPhoneNumberPickerListener()112 public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() { 113 return mListener; 114 } 115 116 @Override onCreateView(LayoutInflater inflater, ViewGroup container)117 protected void onCreateView(LayoutInflater inflater, ViewGroup container) { 118 super.onCreateView(inflater, container); 119 120 View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false); 121 mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding); 122 getListView().addHeaderView(paddingView); 123 124 mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container); 125 updateFilterHeaderView(); 126 127 setVisibleScrollbarEnabled(getVisibleScrollbarEnabled()); 128 } 129 130 @Override onPause()131 public void onPause() { 132 super.onPause(); 133 EnrichedCallComponent.get(getContext()) 134 .getEnrichedCallManager() 135 .unregisterCapabilitiesListener(this); 136 } 137 138 @Override onResume()139 public void onResume() { 140 super.onResume(); 141 EnrichedCallComponent.get(getContext()) 142 .getEnrichedCallManager() 143 .registerCapabilitiesListener(this); 144 } 145 getVisibleScrollbarEnabled()146 protected boolean getVisibleScrollbarEnabled() { 147 return true; 148 } 149 150 @Override setSearchMode(boolean flag)151 protected void setSearchMode(boolean flag) { 152 super.setSearchMode(flag); 153 updateFilterHeaderView(); 154 } 155 updateFilterHeaderView()156 private void updateFilterHeaderView() { 157 final ContactListFilter filter = getFilter(); 158 if (mAccountFilterHeader == null || filter == null) { 159 return; 160 } 161 final boolean shouldShowHeader = 162 !isSearchMode() 163 && AccountFilterUtil.updateAccountFilterTitleForPhone( 164 mAccountFilterHeader, filter, false); 165 if (shouldShowHeader) { 166 mPaddingView.setVisibility(View.GONE); 167 mAccountFilterHeader.setVisibility(View.VISIBLE); 168 } else { 169 mPaddingView.setVisibility(View.VISIBLE); 170 mAccountFilterHeader.setVisibility(View.GONE); 171 } 172 } 173 174 @Override restoreSavedState(Bundle savedState)175 public void restoreSavedState(Bundle savedState) { 176 super.restoreSavedState(savedState); 177 178 if (savedState == null) { 179 return; 180 } 181 182 mFilter = savedState.getParcelable(KEY_FILTER); 183 } 184 185 @Override onSaveInstanceState(Bundle outState)186 public void onSaveInstanceState(Bundle outState) { 187 super.onSaveInstanceState(outState); 188 outState.putParcelable(KEY_FILTER, mFilter); 189 } 190 191 @Override onOptionsItemSelected(MenuItem item)192 public boolean onOptionsItemSelected(MenuItem item) { 193 final int itemId = item.getItemId(); 194 if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled() 195 if (mListener != null) { 196 mListener.onHomeInActionBarSelected(); 197 } 198 return true; 199 } 200 return super.onOptionsItemSelected(item); 201 } 202 203 @Override onItemClick(int position, long id)204 protected void onItemClick(int position, long id) { 205 callNumber(position, false /* isVideoCall */); 206 } 207 208 /** 209 * Initiates a call to the number at the specified position. 210 * 211 * @param position The position. 212 * @param isVideoCall {@code true} if the call should be initiated as a video call, {@code false} 213 * otherwise. 214 */ callNumber(int position, boolean isVideoCall)215 private void callNumber(int position, boolean isVideoCall) { 216 final String number = getPhoneNumber(position); 217 if (!TextUtils.isEmpty(number)) { 218 cacheContactInfo(position); 219 CallSpecificAppData callSpecificAppData = 220 CallSpecificAppData.newBuilder() 221 .setCallInitiationType(getCallInitiationType(true /* isRemoteDirectory */)) 222 .setPositionOfSelectedSearchResult(position) 223 .setCharactersInSearchString(getQueryString() == null ? 0 : getQueryString().length()) 224 .build(); 225 mListener.onPickPhoneNumber(number, isVideoCall, callSpecificAppData); 226 } else { 227 LogUtil.i( 228 "PhoneNumberPickerFragment.callNumber", 229 "item at %d was clicked before adapter is ready, ignoring", 230 position); 231 } 232 233 // Get the lookup key and track any analytics 234 final String lookupKey = getLookupKey(position); 235 if (!TextUtils.isEmpty(lookupKey)) { 236 maybeTrackAnalytics(lookupKey); 237 } 238 } 239 cacheContactInfo(int position)240 protected void cacheContactInfo(int position) { 241 // Not implemented. Hook for child classes 242 } 243 getPhoneNumber(int position)244 protected String getPhoneNumber(int position) { 245 final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); 246 return adapter.getPhoneNumber(position); 247 } 248 getLookupKey(int position)249 protected String getLookupKey(int position) { 250 final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); 251 return adapter.getLookupKey(position); 252 } 253 254 @Override startLoading()255 protected void startLoading() { 256 mLoaderStarted = true; 257 super.startLoading(); 258 } 259 260 @Override 261 @MainThread onLoadFinished(Loader<Cursor> loader, Cursor data)262 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 263 Assert.isMainThread(); 264 // TODO: define and verify behavior for "Nearby places", corp directories, 265 // and dividers listed in UI between these categories 266 if (mCursorReranker != null 267 && data != null 268 && !data.isClosed() 269 && data.getCount() > 0 270 && loader.getId() != -1) { // skip invalid directory ID of -1 271 data = mCursorReranker.rerankCursor(data); 272 } 273 super.onLoadFinished(loader, data); 274 275 // disable scroll bar if there is no data 276 setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0); 277 278 if (data != null) { 279 notifyListeners(); 280 } 281 } 282 283 /** Ranks cursor data rows and returns reference to new cursor object with reordered data. */ 284 public interface CursorReranker { 285 @MainThread rerankCursor(Cursor data)286 Cursor rerankCursor(Cursor data); 287 } 288 289 @MainThread setReranker(@ullable CursorReranker reranker)290 public void setReranker(@Nullable CursorReranker reranker) { 291 Assert.isMainThread(); 292 mCursorReranker = reranker; 293 } 294 295 /** Listener that is notified when cursor has finished loading data. */ 296 public interface OnLoadFinishedListener { onLoadFinished()297 void onLoadFinished(); 298 } 299 300 @MainThread addOnLoadFinishedListener(OnLoadFinishedListener listener)301 public void addOnLoadFinishedListener(OnLoadFinishedListener listener) { 302 Assert.isMainThread(); 303 mLoadFinishedListeners.add(listener); 304 } 305 306 @MainThread removeOnLoadFinishedListener(OnLoadFinishedListener listener)307 public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) { 308 Assert.isMainThread(); 309 mLoadFinishedListeners.remove(listener); 310 } 311 312 @MainThread notifyListeners()313 protected void notifyListeners() { 314 Assert.isMainThread(); 315 for (OnLoadFinishedListener listener : mLoadFinishedListeners) { 316 listener.onLoadFinished(); 317 } 318 } 319 320 @Override onCapabilitiesUpdated()321 public void onCapabilitiesUpdated() { 322 if (getAdapter() != null) { 323 getAdapter().notifyDataSetChanged(); 324 } 325 } 326 327 @MainThread 328 @Override onDetach()329 public void onDetach() { 330 Assert.isMainThread(); 331 mLoadFinishedListeners.clear(); 332 super.onDetach(); 333 } 334 setUseCallableUri(boolean useCallableUri)335 public void setUseCallableUri(boolean useCallableUri) { 336 mUseCallableUri = useCallableUri; 337 } 338 usesCallableUri()339 public boolean usesCallableUri() { 340 return mUseCallableUri; 341 } 342 343 @Override createListAdapter()344 protected ContactEntryListAdapter createListAdapter() { 345 PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity()); 346 adapter.setDisplayPhotos(true); 347 adapter.setUseCallableUri(mUseCallableUri); 348 return adapter; 349 } 350 351 @Override configureAdapter()352 protected void configureAdapter() { 353 super.configureAdapter(); 354 355 final ContactEntryListAdapter adapter = getAdapter(); 356 if (adapter == null) { 357 return; 358 } 359 360 if (!isSearchMode() && mFilter != null) { 361 adapter.setFilter(mFilter); 362 } 363 364 setPhotoPosition(adapter); 365 } 366 setPhotoPosition(ContactEntryListAdapter adapter)367 protected void setPhotoPosition(ContactEntryListAdapter adapter) { 368 ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition); 369 } 370 371 @Override inflateView(LayoutInflater inflater, ViewGroup container)372 protected View inflateView(LayoutInflater inflater, ViewGroup container) { 373 return inflater.inflate(R.layout.contact_list_content, null); 374 } 375 getFilter()376 public ContactListFilter getFilter() { 377 return mFilter; 378 } 379 setFilter(ContactListFilter filter)380 public void setFilter(ContactListFilter filter) { 381 if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) { 382 return; 383 } 384 385 mFilter = filter; 386 if (mLoaderStarted) { 387 reloadData(); 388 } 389 updateFilterHeaderView(); 390 } 391 setPhotoPosition(ContactListItemView.PhotoPosition photoPosition)392 public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { 393 mPhotoPosition = photoPosition; 394 395 final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); 396 if (adapter != null) { 397 adapter.setPhotoPosition(photoPosition); 398 } 399 } 400 401 /** 402 * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number 403 * not in the local contacts database 404 */ getCallInitiationType(boolean isRemoteDirectory)405 protected CallInitiationType.Type getCallInitiationType(boolean isRemoteDirectory) { 406 return Type.UNKNOWN_INITIATION; 407 } 408 409 /** 410 * Where a lookup key contains analytic event information, logs the associated analytics event. 411 * 412 * @param lookupKey The lookup key JSON object. 413 */ maybeTrackAnalytics(String lookupKey)414 private void maybeTrackAnalytics(String lookupKey) { 415 try { 416 JSONObject json = new JSONObject(lookupKey); 417 418 String analyticsCategory = 419 json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY); 420 String analyticsAction = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION); 421 String analyticsValue = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE); 422 423 if (TextUtils.isEmpty(analyticsCategory) 424 || TextUtils.isEmpty(analyticsAction) 425 || TextUtils.isEmpty(analyticsValue)) { 426 return; 427 } 428 429 // Assume that the analytic value being tracked could be a float value, but just cast 430 // to a long so that the analytic server can handle it. 431 long value; 432 try { 433 float floatValue = Float.parseFloat(analyticsValue); 434 value = (long) floatValue; 435 } catch (NumberFormatException nfe) { 436 return; 437 } 438 439 Logger.get(getActivity()) 440 .sendHitEventAnalytics(analyticsCategory, analyticsAction, "" /* label */, value); 441 } catch (JSONException e) { 442 // Not an error; just a lookup key that doesn't have the right information. 443 } 444 } 445 } 446