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