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.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 {
     82         public boolean isActionBarShowing();
     83         public boolean isDialpadShown();
     84         public int getDialpadHeight();
     85         public int getActionBarHideOffset();
     86         public int getActionBarHeight();
     87     }
     88 
     89     @Override
     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
    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
    163     public void onViewCreated(View view, Bundle savedInstanceState) {
    164         super.onViewCreated(view, savedInstanceState);
    165         ViewUtil.addBottomPaddingToListViewForFab(getListView(), getResources());
    166     }
    167 
    168     @Override
    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
    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 
    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      */
    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
    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
    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      */
    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 
    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
    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 
    382     public void setOnTouchListener(View.OnTouchListener onTouchListener) {
    383         mActivityOnTouchListener = onTouchListener;
    384     }
    385 
    386     @Override
    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 
    398     protected void setupEmptyView() {}
    399 }
    400