1 /* 2 * Copyright (C) 2013 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.dialer.app.list; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorInflater; 20 import android.animation.AnimatorListenerAdapter; 21 import android.app.Activity; 22 import android.app.DialogFragment; 23 import android.content.Intent; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.text.TextUtils; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.animation.Interpolator; 31 import android.widget.AbsListView; 32 import android.widget.AbsListView.OnScrollListener; 33 import android.widget.LinearLayout; 34 import android.widget.ListView; 35 import android.widget.Space; 36 import com.android.contacts.common.list.ContactEntryListAdapter; 37 import com.android.contacts.common.list.ContactListItemView; 38 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 39 import com.android.contacts.common.list.PhoneNumberPickerFragment; 40 import com.android.dialer.animation.AnimUtils; 41 import com.android.dialer.app.R; 42 import com.android.dialer.app.dialpad.DialpadFragment.ErrorDialogFragment; 43 import com.android.dialer.app.widget.DialpadSearchEmptyContentView; 44 import com.android.dialer.app.widget.EmptyContentView; 45 import com.android.dialer.callintent.CallSpecificAppData; 46 import com.android.dialer.common.LogUtil; 47 import com.android.dialer.util.DialerUtils; 48 import com.android.dialer.util.IntentUtil; 49 import com.android.dialer.util.PermissionsUtil; 50 51 public class SearchFragment extends PhoneNumberPickerFragment { 52 53 protected EmptyContentView mEmptyView; 54 private OnListFragmentScrolledListener mActivityScrollListener; 55 private View.OnTouchListener mActivityOnTouchListener; 56 /* 57 * Stores the untouched user-entered string that is used to populate the add to contacts 58 * intent. 59 */ 60 private String mAddToContactNumber; 61 private int mActionBarHeight; 62 private int mShadowHeight; 63 private int mPaddingTop; 64 private int mShowDialpadDuration; 65 private int mHideDialpadDuration; 66 /** 67 * Used to resize the list view containing search results so that it fits the available space 68 * above the dialpad. Does not have a user-visible effect in regular touch usage (since the 69 * dialpad hides that portion of the ListView anyway), but improves usability in accessibility 70 * mode. 71 */ 72 private Space mSpacer; 73 74 private HostInterface mActivity; 75 76 @Override onAttach(Activity activity)77 public void onAttach(Activity activity) { 78 super.onAttach(activity); 79 80 setQuickContactEnabled(true); 81 setAdjustSelectionBoundsEnabled(false); 82 setDarkTheme(false); 83 setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(false /* opposite */)); 84 setUseCallableUri(true); 85 86 try { 87 mActivityScrollListener = (OnListFragmentScrolledListener) activity; 88 } catch (ClassCastException e) { 89 LogUtil.v( 90 "SearchFragment.onAttach", 91 activity.toString() 92 + " doesn't implement OnListFragmentScrolledListener. " 93 + "Ignoring."); 94 } 95 } 96 97 @Override onStart()98 public void onStart() { 99 LogUtil.d("SearchFragment.onStart", ""); 100 super.onStart(); 101 if (isSearchMode()) { 102 getAdapter().setHasHeader(0, false); 103 } 104 105 mActivity = (HostInterface) getActivity(); 106 107 final Resources res = getResources(); 108 mActionBarHeight = mActivity.getActionBarHeight(); 109 mShadowHeight = res.getDrawable(R.drawable.search_shadow).getIntrinsicHeight(); 110 mPaddingTop = res.getDimensionPixelSize(R.dimen.search_list_padding_top); 111 mShowDialpadDuration = res.getInteger(R.integer.dialpad_slide_in_duration); 112 mHideDialpadDuration = res.getInteger(R.integer.dialpad_slide_out_duration); 113 114 final ListView listView = getListView(); 115 116 if (mEmptyView == null) { 117 if (this instanceof SmartDialSearchFragment) { 118 mEmptyView = new DialpadSearchEmptyContentView(getActivity()); 119 } else { 120 mEmptyView = new EmptyContentView(getActivity()); 121 } 122 ((ViewGroup) getListView().getParent()).addView(mEmptyView); 123 getListView().setEmptyView(mEmptyView); 124 setupEmptyView(); 125 } 126 127 listView.setBackgroundColor(res.getColor(R.color.background_dialer_results)); 128 listView.setClipToPadding(false); 129 setVisibleScrollbarEnabled(false); 130 131 //Turn of accessibility live region as the list constantly update itself and spam messages. 132 listView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE); 133 ContentChangedFilter.addToParent(listView); 134 135 listView.setOnScrollListener( 136 new OnScrollListener() { 137 @Override 138 public void onScrollStateChanged(AbsListView view, int scrollState) { 139 if (mActivityScrollListener != null) { 140 mActivityScrollListener.onListFragmentScrollStateChange(scrollState); 141 } 142 } 143 144 @Override 145 public void onScroll( 146 AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} 147 }); 148 if (mActivityOnTouchListener != null) { 149 listView.setOnTouchListener(mActivityOnTouchListener); 150 } 151 152 updatePosition(false /* animate */); 153 } 154 155 @Override onCreateAnimator(int transit, boolean enter, int nextAnim)156 public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { 157 Animator animator = null; 158 if (nextAnim != 0) { 159 animator = AnimatorInflater.loadAnimator(getActivity(), nextAnim); 160 } 161 if (animator != null) { 162 final View view = getView(); 163 final int oldLayerType = view.getLayerType(); 164 animator.addListener( 165 new AnimatorListenerAdapter() { 166 @Override 167 public void onAnimationEnd(Animator animation) { 168 view.setLayerType(oldLayerType, null); 169 } 170 }); 171 } 172 return animator; 173 } 174 175 @Override setSearchMode(boolean flag)176 protected void setSearchMode(boolean flag) { 177 super.setSearchMode(flag); 178 // This hides the "All contacts with phone numbers" header in the search fragment 179 final ContactEntryListAdapter adapter = getAdapter(); 180 if (adapter != null) { 181 adapter.setHasHeader(0, false); 182 } 183 } 184 setAddToContactNumber(String addToContactNumber)185 public void setAddToContactNumber(String addToContactNumber) { 186 mAddToContactNumber = addToContactNumber; 187 } 188 189 /** 190 * Return true if phone number is prohibited by a value - 191 * (R.string.config_prohibited_phone_number_regexp) in the config files. False otherwise. 192 */ checkForProhibitedPhoneNumber(String number)193 public boolean checkForProhibitedPhoneNumber(String number) { 194 // Regular expression prohibiting manual phone call. Can be empty i.e. "no rule". 195 String prohibitedPhoneNumberRegexp = 196 getResources().getString(R.string.config_prohibited_phone_number_regexp); 197 198 // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated 199 // test equipment. 200 if (number != null 201 && !TextUtils.isEmpty(prohibitedPhoneNumberRegexp) 202 && number.matches(prohibitedPhoneNumberRegexp)) { 203 LogUtil.i( 204 "SearchFragment.checkForProhibitedPhoneNumber", 205 "the phone number is prohibited explicitly by a rule"); 206 if (getActivity() != null) { 207 DialogFragment dialogFragment = 208 ErrorDialogFragment.newInstance(R.string.dialog_phone_call_prohibited_message); 209 dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog"); 210 } 211 212 return true; 213 } 214 return false; 215 } 216 217 @Override createListAdapter()218 protected ContactEntryListAdapter createListAdapter() { 219 DialerPhoneNumberListAdapter adapter = new DialerPhoneNumberListAdapter(getActivity()); 220 adapter.setDisplayPhotos(true); 221 adapter.setUseCallableUri(super.usesCallableUri()); 222 adapter.setListener(this); 223 return adapter; 224 } 225 226 @Override onItemClick(int position, long id)227 protected void onItemClick(int position, long id) { 228 final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter(); 229 final int shortcutType = adapter.getShortcutTypeFromPosition(position); 230 final OnPhoneNumberPickerActionListener listener; 231 final Intent intent; 232 final String number; 233 234 LogUtil.i("SearchFragment.onItemClick", "shortcutType: " + shortcutType); 235 236 switch (shortcutType) { 237 case DialerPhoneNumberListAdapter.SHORTCUT_DIRECT_CALL: 238 number = adapter.getQueryString(); 239 listener = getOnPhoneNumberPickerListener(); 240 if (listener != null && !checkForProhibitedPhoneNumber(number)) { 241 CallSpecificAppData callSpecificAppData = 242 CallSpecificAppData.newBuilder() 243 .setCallInitiationType(getCallInitiationType(false /* isRemoteDirectory */)) 244 .setPositionOfSelectedSearchResult(position) 245 .setCharactersInSearchString( 246 getQueryString() == null ? 0 : getQueryString().length()) 247 .build(); 248 listener.onPickPhoneNumber(number, false /* isVideoCall */, callSpecificAppData); 249 } 250 break; 251 case DialerPhoneNumberListAdapter.SHORTCUT_CREATE_NEW_CONTACT: 252 number = 253 TextUtils.isEmpty(mAddToContactNumber) 254 ? adapter.getFormattedQueryString() 255 : mAddToContactNumber; 256 intent = IntentUtil.getNewContactIntent(number); 257 DialerUtils.startActivityWithErrorToast(getActivity(), intent); 258 break; 259 case DialerPhoneNumberListAdapter.SHORTCUT_ADD_TO_EXISTING_CONTACT: 260 number = 261 TextUtils.isEmpty(mAddToContactNumber) 262 ? adapter.getFormattedQueryString() 263 : mAddToContactNumber; 264 intent = IntentUtil.getAddToExistingContactIntent(number); 265 DialerUtils.startActivityWithErrorToast( 266 getActivity(), intent, R.string.add_contact_not_available); 267 break; 268 case DialerPhoneNumberListAdapter.SHORTCUT_SEND_SMS_MESSAGE: 269 number = adapter.getFormattedQueryString(); 270 intent = IntentUtil.getSendSmsIntent(number); 271 DialerUtils.startActivityWithErrorToast(getActivity(), intent); 272 break; 273 case DialerPhoneNumberListAdapter.SHORTCUT_MAKE_VIDEO_CALL: 274 number = 275 TextUtils.isEmpty(mAddToContactNumber) ? adapter.getQueryString() : mAddToContactNumber; 276 listener = getOnPhoneNumberPickerListener(); 277 if (listener != null && !checkForProhibitedPhoneNumber(number)) { 278 CallSpecificAppData callSpecificAppData = 279 CallSpecificAppData.newBuilder() 280 .setCallInitiationType(getCallInitiationType(false /* isRemoteDirectory */)) 281 .setPositionOfSelectedSearchResult(position) 282 .setCharactersInSearchString( 283 getQueryString() == null ? 0 : getQueryString().length()) 284 .build(); 285 listener.onPickPhoneNumber(number, true /* isVideoCall */, callSpecificAppData); 286 } 287 break; 288 case DialerPhoneNumberListAdapter.SHORTCUT_INVALID: 289 default: 290 super.onItemClick(position, id); 291 break; 292 } 293 } 294 295 /** 296 * Updates the position and padding of the search fragment, depending on whether the dialpad is 297 * shown. This can be optionally animated. 298 */ updatePosition(boolean animate)299 public void updatePosition(boolean animate) { 300 LogUtil.d("SearchFragment.updatePosition", "animate: %b", animate); 301 if (mActivity == null) { 302 // Activity will be set in onStart, and this method will be called again 303 return; 304 } 305 306 // Use negative shadow height instead of 0 to account for the 9-patch's shadow. 307 int startTranslationValue = 308 mActivity.isDialpadShown() ? mActionBarHeight - mShadowHeight : -mShadowHeight; 309 int endTranslationValue = 0; 310 // Prevents ListView from being translated down after a rotation when the ActionBar is up. 311 if (animate || mActivity.isActionBarShowing()) { 312 endTranslationValue = mActivity.isDialpadShown() ? 0 : mActionBarHeight - mShadowHeight; 313 } 314 if (animate) { 315 // If the dialpad will be shown, then this animation involves sliding the list up. 316 final boolean slideUp = mActivity.isDialpadShown(); 317 318 Interpolator interpolator = slideUp ? AnimUtils.EASE_IN : AnimUtils.EASE_OUT; 319 int duration = slideUp ? mShowDialpadDuration : mHideDialpadDuration; 320 getView().setTranslationY(startTranslationValue); 321 getView() 322 .animate() 323 .translationY(endTranslationValue) 324 .setInterpolator(interpolator) 325 .setDuration(duration) 326 .setListener( 327 new AnimatorListenerAdapter() { 328 @Override 329 public void onAnimationStart(Animator animation) { 330 if (!slideUp) { 331 resizeListView(); 332 } 333 } 334 335 @Override 336 public void onAnimationEnd(Animator animation) { 337 if (slideUp) { 338 resizeListView(); 339 } 340 } 341 }); 342 343 } else { 344 getView().setTranslationY(endTranslationValue); 345 resizeListView(); 346 } 347 348 // There is padding which should only be applied when the dialpad is not shown. 349 int paddingTop = mActivity.isDialpadShown() ? 0 : mPaddingTop; 350 final ListView listView = getListView(); 351 listView.setPaddingRelative( 352 listView.getPaddingStart(), 353 paddingTop, 354 listView.getPaddingEnd(), 355 listView.getPaddingBottom()); 356 } 357 resizeListView()358 public void resizeListView() { 359 if (mSpacer == null) { 360 return; 361 } 362 int spacerHeight = mActivity.isDialpadShown() ? mActivity.getDialpadHeight() : 0; 363 LogUtil.d( 364 "SearchFragment.resizeListView", 365 "spacerHeight: %d -> %d, isDialpadShown: %b, dialpad height: %d", 366 mSpacer.getHeight(), 367 spacerHeight, 368 mActivity.isDialpadShown(), 369 mActivity.getDialpadHeight()); 370 if (spacerHeight != mSpacer.getHeight()) { 371 final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpacer.getLayoutParams(); 372 lp.height = spacerHeight; 373 mSpacer.setLayoutParams(lp); 374 } 375 } 376 377 @Override startLoading()378 protected void startLoading() { 379 if (getActivity() == null) { 380 return; 381 } 382 383 if (PermissionsUtil.hasContactsReadPermissions(getActivity())) { 384 super.startLoading(); 385 } else if (TextUtils.isEmpty(getQueryString())) { 386 // Clear out any existing call shortcuts. 387 final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter(); 388 adapter.disableAllShortcuts(); 389 } else { 390 // The contact list is not going to change (we have no results since permissions are 391 // denied), but the shortcuts might because of the different query, so update the 392 // list. 393 getAdapter().notifyDataSetChanged(); 394 } 395 396 setupEmptyView(); 397 } 398 setOnTouchListener(View.OnTouchListener onTouchListener)399 public void setOnTouchListener(View.OnTouchListener onTouchListener) { 400 mActivityOnTouchListener = onTouchListener; 401 } 402 403 @Override inflateView(LayoutInflater inflater, ViewGroup container)404 protected View inflateView(LayoutInflater inflater, ViewGroup container) { 405 final LinearLayout parent = (LinearLayout) super.inflateView(inflater, container); 406 final int orientation = getResources().getConfiguration().orientation; 407 if (orientation == Configuration.ORIENTATION_PORTRAIT) { 408 mSpacer = new Space(getActivity()); 409 parent.addView( 410 mSpacer, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0)); 411 } 412 return parent; 413 } 414 setupEmptyView()415 protected void setupEmptyView() {} 416 417 public interface HostInterface { 418 isActionBarShowing()419 boolean isActionBarShowing(); 420 isDialpadShown()421 boolean isDialpadShown(); 422 getDialpadHeight()423 int getDialpadHeight(); 424 getActionBarHeight()425 int getActionBarHeight(); 426 } 427 } 428