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