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