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.Intent; 19 import android.content.Loader; 20 import android.database.Cursor; 21 import android.net.Uri; 22 import android.os.Bundle; 23 import android.text.TextUtils; 24 import android.util.Log; 25 import android.view.LayoutInflater; 26 import android.view.MenuItem; 27 import android.view.View; 28 import android.view.View.OnClickListener; 29 import android.view.ViewGroup; 30 31 import com.android.contacts.common.R; 32 import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 33 import com.android.contacts.common.util.AccountFilterUtil; 34 import com.android.contacts.commonbind.analytics.AnalyticsUtil; 35 36 import org.json.JSONException; 37 import org.json.JSONObject; 38 39 /** 40 * Fragment containing a phone number list for picking. 41 */ 42 public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter> 43 implements OnShortcutIntentCreatedListener, PhoneNumberListAdapter.Listener { 44 private static final String TAG = PhoneNumberPickerFragment.class.getSimpleName(); 45 46 private static final int REQUEST_CODE_ACCOUNT_FILTER = 1; 47 48 private static final String KEY_SHORTCUT_ACTION = "shortcutAction"; 49 50 private OnPhoneNumberPickerActionListener mListener; 51 private String mShortcutAction; 52 53 private ContactListFilter mFilter; 54 55 private View mAccountFilterHeader; 56 /** 57 * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set 58 * to View.GONE. 59 */ 60 private View mPaddingView; 61 62 private static final String KEY_FILTER = "filter"; 63 64 /** true if the loader has started at least once. */ 65 private boolean mLoaderStarted; 66 67 private boolean mUseCallableUri; 68 69 private ContactListItemView.PhotoPosition mPhotoPosition = 70 ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */); 71 72 /** 73 * Handles a click on the video call icon for a row in the list. 74 * 75 * @param position The position in the list where the click ocurred. 76 */ 77 @Override onVideoCallIconClicked(int position)78 public void onVideoCallIconClicked(int position) { 79 callNumber(position, true /* isVideoCall */); 80 } 81 82 private class FilterHeaderClickListener implements OnClickListener { 83 @Override onClick(View view)84 public void onClick(View view) { 85 AccountFilterUtil.startAccountFilterActivityForResult( 86 PhoneNumberPickerFragment.this, 87 REQUEST_CODE_ACCOUNT_FILTER, 88 mFilter); 89 } 90 } 91 private OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener(); 92 PhoneNumberPickerFragment()93 public PhoneNumberPickerFragment() { 94 setQuickContactEnabled(false); 95 setPhotoLoaderEnabled(true); 96 setSectionHeaderDisplayEnabled(true); 97 setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); 98 99 // Show nothing instead of letting caller Activity show something. 100 setHasOptionsMenu(true); 101 } 102 setDirectorySearchEnabled(boolean flag)103 public void setDirectorySearchEnabled(boolean flag) { 104 setDirectorySearchMode(flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT 105 : 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 mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener); 126 updateFilterHeaderView(); 127 128 setVisibleScrollbarEnabled(getVisibleScrollbarEnabled()); 129 } 130 getVisibleScrollbarEnabled()131 protected boolean getVisibleScrollbarEnabled() { 132 return true; 133 } 134 135 @Override setSearchMode(boolean flag)136 protected void setSearchMode(boolean flag) { 137 super.setSearchMode(flag); 138 updateFilterHeaderView(); 139 } 140 updateFilterHeaderView()141 private void updateFilterHeaderView() { 142 final ContactListFilter filter = getFilter(); 143 if (mAccountFilterHeader == null || filter == null) { 144 return; 145 } 146 final boolean shouldShowHeader = 147 !isSearchMode() && 148 AccountFilterUtil.updateAccountFilterTitleForPhone( 149 mAccountFilterHeader, filter, false); 150 if (shouldShowHeader) { 151 mPaddingView.setVisibility(View.GONE); 152 mAccountFilterHeader.setVisibility(View.VISIBLE); 153 } else { 154 mPaddingView.setVisibility(View.VISIBLE); 155 mAccountFilterHeader.setVisibility(View.GONE); 156 } 157 } 158 159 @Override restoreSavedState(Bundle savedState)160 public void restoreSavedState(Bundle savedState) { 161 super.restoreSavedState(savedState); 162 163 if (savedState == null) { 164 return; 165 } 166 167 mFilter = savedState.getParcelable(KEY_FILTER); 168 mShortcutAction = savedState.getString(KEY_SHORTCUT_ACTION); 169 } 170 171 @Override onSaveInstanceState(Bundle outState)172 public void onSaveInstanceState(Bundle outState) { 173 super.onSaveInstanceState(outState); 174 outState.putParcelable(KEY_FILTER, mFilter); 175 outState.putString(KEY_SHORTCUT_ACTION, mShortcutAction); 176 } 177 178 @Override onOptionsItemSelected(MenuItem item)179 public boolean onOptionsItemSelected(MenuItem item) { 180 final int itemId = item.getItemId(); 181 if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled() 182 if (mListener != null) { 183 mListener.onHomeInActionBarSelected(); 184 } 185 return true; 186 } 187 return super.onOptionsItemSelected(item); 188 } 189 190 /** 191 * @param shortcutAction either {@link Intent#ACTION_CALL} or 192 * {@link Intent#ACTION_SENDTO} or null. 193 */ setShortcutAction(String shortcutAction)194 public void setShortcutAction(String shortcutAction) { 195 this.mShortcutAction = shortcutAction; 196 } 197 198 @Override onItemClick(int position, long id)199 protected void onItemClick(int position, long id) { 200 callNumber(position, false /* isVideoCall */); 201 } 202 203 /** 204 * Initiates a call to the number at the specified position. 205 * 206 * @param position The position. 207 * @param isVideoCall {@code true} if the call should be initiated as a video call, 208 * {@code false} otherwise. 209 */ callNumber(int position, boolean isVideoCall)210 private void callNumber(int position, boolean isVideoCall) { 211 final Uri phoneUri = getPhoneUri(position); 212 213 if (phoneUri != null) { 214 pickPhoneNumber(phoneUri, isVideoCall); 215 } else { 216 final String number = getPhoneNumber(position); 217 if (!TextUtils.isEmpty(number)) { 218 cacheContactInfo(position); 219 mListener.onPickPhoneNumber(number, isVideoCall, 220 getCallInitiationType(true /* isRemoteDirectory */)); 221 } else { 222 Log.w(TAG, "Item at " + position + " was clicked before" 223 + " adapter is ready. Ignoring"); 224 } 225 } 226 227 // Get the lookup key and track any analytics 228 final String lookupKey = getLookupKey(position); 229 if (!TextUtils.isEmpty(lookupKey)) { 230 maybeTrackAnalytics(lookupKey); 231 } 232 } 233 cacheContactInfo(int position)234 protected void cacheContactInfo(int position) { 235 // Not implemented. Hook for child classes 236 } 237 getPhoneNumber(int position)238 protected String getPhoneNumber(int position) { 239 final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); 240 return adapter.getPhoneNumber(position); 241 } 242 getPhoneUri(int position)243 protected Uri getPhoneUri(int position) { 244 final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); 245 return adapter.getDataUri(position); 246 } 247 getLookupKey(int position)248 protected String getLookupKey(int position) { 249 final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); 250 return adapter.getLookupKey(position); 251 } 252 253 @Override startLoading()254 protected void startLoading() { 255 mLoaderStarted = true; 256 super.startLoading(); 257 } 258 259 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)260 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 261 super.onLoadFinished(loader, data); 262 263 // disable scroll bar if there is no data 264 setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0); 265 } 266 setUseCallableUri(boolean useCallableUri)267 public void setUseCallableUri(boolean useCallableUri) { 268 mUseCallableUri = useCallableUri; 269 } 270 usesCallableUri()271 public boolean usesCallableUri() { 272 return mUseCallableUri; 273 } 274 275 @Override createListAdapter()276 protected ContactEntryListAdapter createListAdapter() { 277 PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity()); 278 adapter.setDisplayPhotos(true); 279 adapter.setUseCallableUri(mUseCallableUri); 280 return adapter; 281 } 282 283 @Override configureAdapter()284 protected void configureAdapter() { 285 super.configureAdapter(); 286 287 final ContactEntryListAdapter adapter = getAdapter(); 288 if (adapter == null) { 289 return; 290 } 291 292 if (!isSearchMode() && mFilter != null) { 293 adapter.setFilter(mFilter); 294 } 295 296 setPhotoPosition(adapter); 297 } 298 setPhotoPosition(ContactEntryListAdapter adapter)299 protected void setPhotoPosition(ContactEntryListAdapter adapter) { 300 ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition); 301 } 302 303 @Override inflateView(LayoutInflater inflater, ViewGroup container)304 protected View inflateView(LayoutInflater inflater, ViewGroup container) { 305 return inflater.inflate(R.layout.contact_list_content, null); 306 } 307 pickPhoneNumber(Uri uri, boolean isVideoCall)308 public void pickPhoneNumber(Uri uri, boolean isVideoCall) { 309 if (mShortcutAction == null) { 310 mListener.onPickDataUri(uri, isVideoCall, 311 getCallInitiationType(false /* isRemoteDirectory */)); 312 } else { 313 startPhoneNumberShortcutIntent(uri, isVideoCall); 314 } 315 } 316 startPhoneNumberShortcutIntent(Uri uri, boolean isVideoCall)317 protected void startPhoneNumberShortcutIntent(Uri uri, boolean isVideoCall) { 318 ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this); 319 builder.createPhoneNumberShortcutIntent(uri, mShortcutAction); 320 } 321 322 @Override onShortcutIntentCreated(Uri uri, Intent shortcutIntent)323 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 324 mListener.onShortcutIntentCreated(shortcutIntent); 325 } 326 327 @Override onPickerResult(Intent data)328 public void onPickerResult(Intent data) { 329 mListener.onPickDataUri(data.getData(), false /* isVideoCall */, 330 getCallInitiationType(false /* isRemoteDirectory */)); 331 } 332 333 @Override onActivityResult(int requestCode, int resultCode, Intent data)334 public void onActivityResult(int requestCode, int resultCode, Intent data) { 335 if (requestCode == REQUEST_CODE_ACCOUNT_FILTER) { 336 if (getActivity() != null) { 337 AccountFilterUtil.handleAccountFilterResult( 338 ContactListFilterController.getInstance(getActivity()), resultCode, data); 339 } else { 340 Log.e(TAG, "getActivity() returns null during Fragment#onActivityResult()"); 341 } 342 } 343 } 344 getFilter()345 public ContactListFilter getFilter() { 346 return mFilter; 347 } 348 setFilter(ContactListFilter filter)349 public void setFilter(ContactListFilter filter) { 350 if ((mFilter == null && filter == null) || 351 (mFilter != null && mFilter.equals(filter))) { 352 return; 353 } 354 355 mFilter = filter; 356 if (mLoaderStarted) { 357 reloadData(); 358 } 359 updateFilterHeaderView(); 360 } 361 setPhotoPosition(ContactListItemView.PhotoPosition photoPosition)362 public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { 363 mPhotoPosition = photoPosition; 364 365 final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); 366 if (adapter != null) { 367 adapter.setPhotoPosition(photoPosition); 368 } 369 } 370 371 /** 372 * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number 373 * not in the local contacts database 374 */ getCallInitiationType(boolean isRemoteDirectory)375 protected int getCallInitiationType(boolean isRemoteDirectory) { 376 return OnPhoneNumberPickerActionListener.CALL_INITIATION_UNKNOWN; 377 } 378 379 /** 380 * Where a lookup key contains analytic event information, logs the associated analytics event. 381 * 382 * @param lookupKey The lookup key JSON object. 383 */ maybeTrackAnalytics(String lookupKey)384 private void maybeTrackAnalytics(String lookupKey) { 385 try { 386 JSONObject json = new JSONObject(lookupKey); 387 388 String analyticsCategory = json.getString( 389 PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY); 390 String analyticsAction = json.getString( 391 PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION); 392 String analyticsValue = json.getString( 393 PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE); 394 395 if (TextUtils.isEmpty(analyticsCategory) || TextUtils.isEmpty(analyticsAction) || 396 TextUtils.isEmpty(analyticsValue)) { 397 return; 398 } 399 400 // Assume that the analytic value being tracked could be a float value, but just cast 401 // to a long so that the analytic server can handle it. 402 long value; 403 try { 404 float floatValue = Float.parseFloat(analyticsValue); 405 value = (long) floatValue; 406 } catch (NumberFormatException nfe) { 407 return; 408 } 409 410 AnalyticsUtil.sendEvent(getActivity().getApplication(), analyticsCategory, 411 analyticsAction, "" /* label */, value); 412 } catch (JSONException e) { 413 // Not an error; just a lookup key that doesn't have the right information. 414 } 415 } 416 } 417