• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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