Home | History | Annotate | Download | only in list
      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
     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
     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
    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
    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 
    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    */
    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
    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
    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    */
    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 
    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
    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 
    399   public void setOnTouchListener(View.OnTouchListener onTouchListener) {
    400     mActivityOnTouchListener = onTouchListener;
    401   }
    402 
    403   @Override
    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 
    415   protected void setupEmptyView() {}
    416 
    417   public interface HostInterface {
    418 
    419     boolean isActionBarShowing();
    420 
    421     boolean isDialpadShown();
    422 
    423     int getDialpadHeight();
    424 
    425     int getActionBarHeight();
    426   }
    427 }
    428