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
onAttach(Activity activity)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
onStart()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
onCreateAnimator(int transit, boolean enter, int nextAnim)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
setSearchMode(boolean flag)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 
setAddToContactNumber(String addToContactNumber)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    */
checkForProhibitedPhoneNumber(String number)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
createListAdapter()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
onItemClick(int position, long id)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    */
updatePosition(boolean animate)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 
resizeListView()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
startLoading()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 
setOnTouchListener(View.OnTouchListener onTouchListener)399   public void setOnTouchListener(View.OnTouchListener onTouchListener) {
400     mActivityOnTouchListener = onTouchListener;
401   }
402 
403   @Override
inflateView(LayoutInflater inflater, ViewGroup container)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 
setupEmptyView()415   protected void setupEmptyView() {}
416 
417   public interface HostInterface {
418 
isActionBarShowing()419     boolean isActionBarShowing();
420 
isDialpadShown()421     boolean isDialpadShown();
422 
getDialpadHeight()423     int getDialpadHeight();
424 
getActionBarHeight()425     int getActionBarHeight();
426   }
427 }
428