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