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 {
isActionBarShowing()82         public boolean isActionBarShowing();
isDialpadShown()83         public boolean isDialpadShown();
getDialpadHeight()84         public int getDialpadHeight();
getActionBarHideOffset()85         public int getActionBarHideOffset();
getActionBarHeight()86         public int getActionBarHeight();
87     }
88 
89     @Override
onAttach(Activity activity)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
onStart()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
onViewCreated(View view, Bundle savedInstanceState)163     public void onViewCreated(View view, Bundle savedInstanceState) {
164         super.onViewCreated(view, savedInstanceState);
165         ViewUtil.addBottomPaddingToListViewForFab(getListView(), getResources());
166     }
167 
168     @Override
onCreateAnimator(int transit, boolean enter, int nextAnim)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
setSearchMode(boolean flag)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 
setAddToContactNumber(String addToContactNumber)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      */
checkForProhibitedPhoneNumber(String number)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
createListAdapter()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
onItemClick(int position, long id)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      */
updatePosition(boolean animate)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 
resizeListView()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
startLoading()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 
setOnTouchListener(View.OnTouchListener onTouchListener)382     public void setOnTouchListener(View.OnTouchListener onTouchListener) {
383         mActivityOnTouchListener = onTouchListener;
384     }
385 
386     @Override
inflateView(LayoutInflater inflater, ViewGroup container)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 
setupEmptyView()398     protected void setupEmptyView() {}
399 }
400