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.widget.DialpadSearchEmptyContentView; 42 import com.android.dialer.callintent.CallSpecificAppData; 43 import com.android.dialer.common.LogUtil; 44 import com.android.dialer.dialpadview.DialpadFragment.ErrorDialogFragment; 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 emptyView; 55 private OnListFragmentScrolledListener activityScrollListener; 56 private View.OnTouchListener activityOnTouchListener; 57 /* 58 * Stores the untouched user-entered string that is used to populate the add to contacts 59 * intent. 60 */ 61 private String addToContactNumber; 62 private int actionBarHeight; 63 private int shadowHeight; 64 private int paddingTop; 65 private int showDialpadDuration; 66 private int hideDialpadDuration; 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 spacer; 74 75 private HostInterface activity; 76 77 @Override onAttach(Activity activity)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 activityScrollListener = (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 102 activity = (HostInterface) getActivity(); 103 104 final Resources res = getResources(); 105 actionBarHeight = activity.getActionBarHeight(); 106 shadowHeight = res.getDrawable(R.drawable.search_shadow).getIntrinsicHeight(); 107 paddingTop = res.getDimensionPixelSize(R.dimen.search_list_padding_top); 108 showDialpadDuration = res.getInteger(R.integer.dialpad_slide_in_duration); 109 hideDialpadDuration = res.getInteger(R.integer.dialpad_slide_out_duration); 110 111 final ListView listView = getListView(); 112 113 if (emptyView == null) { 114 if (this instanceof SmartDialSearchFragment) { 115 emptyView = new DialpadSearchEmptyContentView(getActivity()); 116 } else { 117 emptyView = new EmptyContentView(getActivity()); 118 } 119 ((ViewGroup) getListView().getParent()).addView(emptyView); 120 getListView().setEmptyView(emptyView); 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 (activityScrollListener != null) { 137 activityScrollListener.onListFragmentScrollStateChange(scrollState); 138 } 139 } 140 141 @Override 142 public void onScroll( 143 AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} 144 }); 145 if (activityOnTouchListener != null) { 146 listView.setOnTouchListener(activityOnTouchListener); 147 } 148 149 updatePosition(false /* animate */); 150 } 151 152 @Override onCreateAnimator(int transit, boolean enter, int nextAnim)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 setAddToContactNumber(String addToContactNumber)172 public void setAddToContactNumber(String addToContactNumber) { 173 this.addToContactNumber = 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 */ checkForProhibitedPhoneNumber(String number)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 createListAdapter()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 onItemClick(int position, long id)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(addToContactNumber) 245 ? adapter.getFormattedQueryString() 246 : addToContactNumber; 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(addToContactNumber) 257 ? adapter.getFormattedQueryString() 258 : addToContactNumber; 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 = 265 TextUtils.isEmpty(addToContactNumber) 266 ? adapter.getFormattedQueryString() 267 : addToContactNumber; 268 intent = IntentUtil.getSendSmsIntent(number); 269 DialerUtils.startActivityWithErrorToast(getActivity(), intent); 270 break; 271 case DialerPhoneNumberListAdapter.SHORTCUT_MAKE_VIDEO_CALL: 272 number = 273 TextUtils.isEmpty(addToContactNumber) ? adapter.getQueryString() : addToContactNumber; 274 listener = getOnPhoneNumberPickerListener(); 275 if (listener != null && !checkForProhibitedPhoneNumber(number)) { 276 CallSpecificAppData callSpecificAppData = 277 CallSpecificAppData.newBuilder() 278 .setCallInitiationType(getCallInitiationType(false /* isRemoteDirectory */)) 279 .setPositionOfSelectedSearchResult(position) 280 .setCharactersInSearchString( 281 getQueryString() == null ? 0 : getQueryString().length()) 282 .build(); 283 listener.onPickPhoneNumber(number, true /* isVideoCall */, callSpecificAppData); 284 } 285 break; 286 case DialerPhoneNumberListAdapter.SHORTCUT_INVALID: 287 default: 288 super.onItemClick(position, id); 289 break; 290 } 291 } 292 293 /** 294 * Updates the position and padding of the search fragment, depending on whether the dialpad is 295 * shown. This can be optionally animated. 296 */ updatePosition(boolean animate)297 public void updatePosition(boolean animate) { 298 LogUtil.d("SearchFragment.updatePosition", "animate: %b", animate); 299 if (activity == null) { 300 // Activity will be set in onStart, and this method will be called again 301 return; 302 } 303 304 // Use negative shadow height instead of 0 to account for the 9-patch's shadow. 305 int startTranslationValue = 306 activity.isDialpadShown() ? actionBarHeight - shadowHeight : -shadowHeight; 307 int endTranslationValue = 0; 308 // Prevents ListView from being translated down after a rotation when the ActionBar is up. 309 if (animate || activity.isActionBarShowing()) { 310 endTranslationValue = activity.isDialpadShown() ? 0 : actionBarHeight - shadowHeight; 311 } 312 if (animate) { 313 // If the dialpad will be shown, then this animation involves sliding the list up. 314 final boolean slideUp = activity.isDialpadShown(); 315 316 Interpolator interpolator = slideUp ? AnimUtils.EASE_IN : AnimUtils.EASE_OUT; 317 int duration = slideUp ? showDialpadDuration : hideDialpadDuration; 318 getView().setTranslationY(startTranslationValue); 319 getView() 320 .animate() 321 .translationY(endTranslationValue) 322 .setInterpolator(interpolator) 323 .setDuration(duration) 324 .setListener( 325 new AnimatorListenerAdapter() { 326 @Override 327 public void onAnimationStart(Animator animation) { 328 if (!slideUp) { 329 resizeListView(); 330 } 331 } 332 333 @Override 334 public void onAnimationEnd(Animator animation) { 335 if (slideUp) { 336 resizeListView(); 337 } 338 } 339 }); 340 341 } else { 342 getView().setTranslationY(endTranslationValue); 343 resizeListView(); 344 } 345 346 // There is padding which should only be applied when the dialpad is not shown. 347 int paddingTop = activity.isDialpadShown() ? 0 : this.paddingTop; 348 final ListView listView = getListView(); 349 listView.setPaddingRelative( 350 listView.getPaddingStart(), 351 paddingTop, 352 listView.getPaddingEnd(), 353 listView.getPaddingBottom()); 354 } 355 resizeListView()356 public void resizeListView() { 357 if (spacer == null) { 358 return; 359 } 360 int spacerHeight = activity.isDialpadShown() ? activity.getDialpadHeight() : 0; 361 LogUtil.d( 362 "SearchFragment.resizeListView", 363 "spacerHeight: %d -> %d, isDialpadShown: %b, dialpad height: %d", 364 spacer.getHeight(), 365 spacerHeight, 366 activity.isDialpadShown(), 367 activity.getDialpadHeight()); 368 if (spacerHeight != spacer.getHeight()) { 369 final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) spacer.getLayoutParams(); 370 lp.height = spacerHeight; 371 spacer.setLayoutParams(lp); 372 } 373 } 374 375 @Override startLoading()376 protected void startLoading() { 377 if (getActivity() == null) { 378 return; 379 } 380 381 if (PermissionsUtil.hasContactsReadPermissions(getActivity())) { 382 super.startLoading(); 383 } else if (TextUtils.isEmpty(getQueryString())) { 384 // Clear out any existing call shortcuts. 385 final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter(); 386 adapter.disableAllShortcuts(); 387 } else { 388 // The contact list is not going to change (we have no results since permissions are 389 // denied), but the shortcuts might because of the different query, so update the 390 // list. 391 getAdapter().notifyDataSetChanged(); 392 } 393 394 setupEmptyView(); 395 } 396 setOnTouchListener(View.OnTouchListener onTouchListener)397 public void setOnTouchListener(View.OnTouchListener onTouchListener) { 398 activityOnTouchListener = onTouchListener; 399 } 400 401 @Override inflateView(LayoutInflater inflater, ViewGroup container)402 protected View inflateView(LayoutInflater inflater, ViewGroup container) { 403 final LinearLayout parent = (LinearLayout) super.inflateView(inflater, container); 404 final int orientation = getResources().getConfiguration().orientation; 405 if (orientation == Configuration.ORIENTATION_PORTRAIT) { 406 spacer = new Space(getActivity()); 407 parent.addView( 408 spacer, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0)); 409 } 410 return parent; 411 } 412 setupEmptyView()413 protected void setupEmptyView() {} 414 415 public interface HostInterface { 416 isActionBarShowing()417 boolean isActionBarShowing(); 418 isDialpadShown()419 boolean isDialpadShown(); 420 getDialpadHeight()421 int getDialpadHeight(); 422 getActionBarHeight()423 int getActionBarHeight(); 424 } 425 } 426