1 /*
2  * Copyright (C) 2010 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.contacts.common.list;
17 
18 import android.content.Intent;
19 import android.content.Loader;
20 import android.database.Cursor;
21 import android.net.Uri;
22 import android.os.Bundle;
23 import android.text.TextUtils;
24 import android.util.Log;
25 import android.view.LayoutInflater;
26 import android.view.MenuItem;
27 import android.view.View;
28 import android.view.View.OnClickListener;
29 import android.view.ViewGroup;
30 
31 import com.android.contacts.common.R;
32 import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
33 import com.android.contacts.common.util.AccountFilterUtil;
34 import com.android.contacts.commonbind.analytics.AnalyticsUtil;
35 
36 import org.json.JSONException;
37 import org.json.JSONObject;
38 
39 /**
40  * Fragment containing a phone number list for picking.
41  */
42 public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
43         implements OnShortcutIntentCreatedListener, PhoneNumberListAdapter.Listener {
44     private static final String TAG = PhoneNumberPickerFragment.class.getSimpleName();
45 
46     private static final int REQUEST_CODE_ACCOUNT_FILTER = 1;
47 
48     private static final String KEY_SHORTCUT_ACTION = "shortcutAction";
49 
50     private OnPhoneNumberPickerActionListener mListener;
51     private String mShortcutAction;
52 
53     private ContactListFilter mFilter;
54 
55     private View mAccountFilterHeader;
56     /**
57      * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set
58      * to View.GONE.
59      */
60     private View mPaddingView;
61 
62     private static final String KEY_FILTER = "filter";
63 
64     /** true if the loader has started at least once. */
65     private boolean mLoaderStarted;
66 
67     private boolean mUseCallableUri;
68 
69     private ContactListItemView.PhotoPosition mPhotoPosition =
70             ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */);
71 
72     /**
73      * Handles a click on the video call icon for a row in the list.
74      *
75      * @param position The position in the list where the click ocurred.
76      */
77     @Override
onVideoCallIconClicked(int position)78     public void onVideoCallIconClicked(int position) {
79         callNumber(position, true /* isVideoCall */);
80     }
81 
82     private class FilterHeaderClickListener implements OnClickListener {
83         @Override
onClick(View view)84         public void onClick(View view) {
85             AccountFilterUtil.startAccountFilterActivityForResult(
86                     PhoneNumberPickerFragment.this,
87                     REQUEST_CODE_ACCOUNT_FILTER,
88                     mFilter);
89         }
90     }
91     private OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener();
92 
PhoneNumberPickerFragment()93     public PhoneNumberPickerFragment() {
94         setQuickContactEnabled(false);
95         setPhotoLoaderEnabled(true);
96         setSectionHeaderDisplayEnabled(true);
97         setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
98 
99         // Show nothing instead of letting caller Activity show something.
100         setHasOptionsMenu(true);
101     }
102 
setDirectorySearchEnabled(boolean flag)103     public void setDirectorySearchEnabled(boolean flag) {
104         setDirectorySearchMode(flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT
105                 : DirectoryListLoader.SEARCH_MODE_NONE);
106     }
107 
setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener)108     public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) {
109         this.mListener = listener;
110     }
111 
getOnPhoneNumberPickerListener()112     public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() {
113         return mListener;
114     }
115 
116     @Override
onCreateView(LayoutInflater inflater, ViewGroup container)117     protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
118         super.onCreateView(inflater, container);
119 
120         View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false);
121         mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding);
122         getListView().addHeaderView(paddingView);
123 
124         mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container);
125         mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
126         updateFilterHeaderView();
127 
128         setVisibleScrollbarEnabled(getVisibleScrollbarEnabled());
129     }
130 
getVisibleScrollbarEnabled()131     protected boolean getVisibleScrollbarEnabled() {
132         return true;
133     }
134 
135     @Override
setSearchMode(boolean flag)136     protected void setSearchMode(boolean flag) {
137         super.setSearchMode(flag);
138         updateFilterHeaderView();
139     }
140 
updateFilterHeaderView()141     private void updateFilterHeaderView() {
142         final ContactListFilter filter = getFilter();
143         if (mAccountFilterHeader == null || filter == null) {
144             return;
145         }
146         final boolean shouldShowHeader =
147                 !isSearchMode() &&
148                 AccountFilterUtil.updateAccountFilterTitleForPhone(
149                         mAccountFilterHeader, filter, false);
150         if (shouldShowHeader) {
151             mPaddingView.setVisibility(View.GONE);
152             mAccountFilterHeader.setVisibility(View.VISIBLE);
153         } else {
154             mPaddingView.setVisibility(View.VISIBLE);
155             mAccountFilterHeader.setVisibility(View.GONE);
156         }
157     }
158 
159     @Override
restoreSavedState(Bundle savedState)160     public void restoreSavedState(Bundle savedState) {
161         super.restoreSavedState(savedState);
162 
163         if (savedState == null) {
164             return;
165         }
166 
167         mFilter = savedState.getParcelable(KEY_FILTER);
168         mShortcutAction = savedState.getString(KEY_SHORTCUT_ACTION);
169     }
170 
171     @Override
onSaveInstanceState(Bundle outState)172     public void onSaveInstanceState(Bundle outState) {
173         super.onSaveInstanceState(outState);
174         outState.putParcelable(KEY_FILTER, mFilter);
175         outState.putString(KEY_SHORTCUT_ACTION, mShortcutAction);
176     }
177 
178     @Override
onOptionsItemSelected(MenuItem item)179     public boolean onOptionsItemSelected(MenuItem item) {
180         final int itemId = item.getItemId();
181         if (itemId == android.R.id.home) {  // See ActionBar#setDisplayHomeAsUpEnabled()
182             if (mListener != null) {
183                 mListener.onHomeInActionBarSelected();
184             }
185             return true;
186         }
187         return super.onOptionsItemSelected(item);
188     }
189 
190     /**
191      * @param shortcutAction either {@link Intent#ACTION_CALL} or
192      *            {@link Intent#ACTION_SENDTO} or null.
193      */
setShortcutAction(String shortcutAction)194     public void setShortcutAction(String shortcutAction) {
195         this.mShortcutAction = shortcutAction;
196     }
197 
198     @Override
onItemClick(int position, long id)199     protected void onItemClick(int position, long id) {
200         callNumber(position, false /* isVideoCall */);
201     }
202 
203     /**
204      * Initiates a call to the number at the specified position.
205      *
206      * @param position The position.
207      * @param isVideoCall {@code true} if the call should be initiated as a video call,
208      *      {@code false} otherwise.
209      */
callNumber(int position, boolean isVideoCall)210     private void callNumber(int position, boolean isVideoCall) {
211         final Uri phoneUri = getPhoneUri(position);
212 
213         if (phoneUri != null) {
214             pickPhoneNumber(phoneUri, isVideoCall);
215         } else {
216             final String number = getPhoneNumber(position);
217             if (!TextUtils.isEmpty(number)) {
218                 cacheContactInfo(position);
219                 mListener.onPickPhoneNumber(number, isVideoCall,
220                         getCallInitiationType(true /* isRemoteDirectory */));
221             } else {
222                 Log.w(TAG, "Item at " + position + " was clicked before"
223                         + " adapter is ready. Ignoring");
224             }
225         }
226 
227         // Get the lookup key and track any analytics
228         final String lookupKey = getLookupKey(position);
229         if (!TextUtils.isEmpty(lookupKey)) {
230             maybeTrackAnalytics(lookupKey);
231         }
232     }
233 
cacheContactInfo(int position)234     protected void cacheContactInfo(int position) {
235         // Not implemented. Hook for child classes
236     }
237 
getPhoneNumber(int position)238     protected String getPhoneNumber(int position) {
239         final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
240         return adapter.getPhoneNumber(position);
241     }
242 
getPhoneUri(int position)243     protected Uri getPhoneUri(int position) {
244         final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
245         return adapter.getDataUri(position);
246     }
247 
getLookupKey(int position)248     protected String getLookupKey(int position) {
249         final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
250         return adapter.getLookupKey(position);
251     }
252 
253     @Override
startLoading()254     protected void startLoading() {
255         mLoaderStarted = true;
256         super.startLoading();
257     }
258 
259     @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)260     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
261         super.onLoadFinished(loader, data);
262 
263         // disable scroll bar if there is no data
264         setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0);
265     }
266 
setUseCallableUri(boolean useCallableUri)267     public void setUseCallableUri(boolean useCallableUri) {
268         mUseCallableUri = useCallableUri;
269     }
270 
usesCallableUri()271     public boolean usesCallableUri() {
272         return mUseCallableUri;
273     }
274 
275     @Override
createListAdapter()276     protected ContactEntryListAdapter createListAdapter() {
277         PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
278         adapter.setDisplayPhotos(true);
279         adapter.setUseCallableUri(mUseCallableUri);
280         return adapter;
281     }
282 
283     @Override
configureAdapter()284     protected void configureAdapter() {
285         super.configureAdapter();
286 
287         final ContactEntryListAdapter adapter = getAdapter();
288         if (adapter == null) {
289             return;
290         }
291 
292         if (!isSearchMode() && mFilter != null) {
293             adapter.setFilter(mFilter);
294         }
295 
296         setPhotoPosition(adapter);
297     }
298 
setPhotoPosition(ContactEntryListAdapter adapter)299     protected void setPhotoPosition(ContactEntryListAdapter adapter) {
300         ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition);
301     }
302 
303     @Override
inflateView(LayoutInflater inflater, ViewGroup container)304     protected View inflateView(LayoutInflater inflater, ViewGroup container) {
305         return inflater.inflate(R.layout.contact_list_content, null);
306     }
307 
pickPhoneNumber(Uri uri, boolean isVideoCall)308     public void pickPhoneNumber(Uri uri, boolean isVideoCall) {
309         if (mShortcutAction == null) {
310             mListener.onPickDataUri(uri, isVideoCall,
311                     getCallInitiationType(false /* isRemoteDirectory */));
312         } else {
313             startPhoneNumberShortcutIntent(uri, isVideoCall);
314         }
315     }
316 
startPhoneNumberShortcutIntent(Uri uri, boolean isVideoCall)317     protected void startPhoneNumberShortcutIntent(Uri uri, boolean isVideoCall) {
318         ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this);
319         builder.createPhoneNumberShortcutIntent(uri, mShortcutAction);
320     }
321 
322     @Override
onShortcutIntentCreated(Uri uri, Intent shortcutIntent)323     public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
324         mListener.onShortcutIntentCreated(shortcutIntent);
325     }
326 
327     @Override
onPickerResult(Intent data)328     public void onPickerResult(Intent data) {
329         mListener.onPickDataUri(data.getData(), false /* isVideoCall */,
330                 getCallInitiationType(false /* isRemoteDirectory */));
331     }
332 
333     @Override
onActivityResult(int requestCode, int resultCode, Intent data)334     public void onActivityResult(int requestCode, int resultCode, Intent data) {
335         if (requestCode == REQUEST_CODE_ACCOUNT_FILTER) {
336             if (getActivity() != null) {
337                 AccountFilterUtil.handleAccountFilterResult(
338                         ContactListFilterController.getInstance(getActivity()), resultCode, data);
339             } else {
340                 Log.e(TAG, "getActivity() returns null during Fragment#onActivityResult()");
341             }
342         }
343     }
344 
getFilter()345     public ContactListFilter getFilter() {
346         return mFilter;
347     }
348 
setFilter(ContactListFilter filter)349     public void setFilter(ContactListFilter filter) {
350         if ((mFilter == null && filter == null) ||
351                 (mFilter != null && mFilter.equals(filter))) {
352             return;
353         }
354 
355         mFilter = filter;
356         if (mLoaderStarted) {
357             reloadData();
358         }
359         updateFilterHeaderView();
360     }
361 
setPhotoPosition(ContactListItemView.PhotoPosition photoPosition)362     public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
363         mPhotoPosition = photoPosition;
364 
365         final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
366         if (adapter != null) {
367             adapter.setPhotoPosition(photoPosition);
368         }
369     }
370 
371     /**
372      * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number
373      *         not in the local contacts database
374      */
getCallInitiationType(boolean isRemoteDirectory)375     protected int getCallInitiationType(boolean isRemoteDirectory) {
376         return OnPhoneNumberPickerActionListener.CALL_INITIATION_UNKNOWN;
377     }
378 
379     /**
380      * Where a lookup key contains analytic event information, logs the associated analytics event.
381      *
382      * @param lookupKey The lookup key JSON object.
383      */
maybeTrackAnalytics(String lookupKey)384     private void maybeTrackAnalytics(String lookupKey) {
385         try {
386             JSONObject json = new JSONObject(lookupKey);
387 
388             String analyticsCategory = json.getString(
389                     PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY);
390             String analyticsAction = json.getString(
391                     PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION);
392             String analyticsValue = json.getString(
393                     PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE);
394 
395             if (TextUtils.isEmpty(analyticsCategory) || TextUtils.isEmpty(analyticsAction) ||
396                     TextUtils.isEmpty(analyticsValue)) {
397                 return;
398             }
399 
400             // Assume that the analytic value being tracked could be a float value, but just cast
401             // to a long so that the analytic server can handle it.
402             long value;
403             try {
404                 float floatValue = Float.parseFloat(analyticsValue);
405                 value = (long) floatValue;
406             } catch (NumberFormatException nfe) {
407                 return;
408             }
409 
410             AnalyticsUtil.sendEvent(getActivity().getApplication(), analyticsCategory,
411                     analyticsAction, "" /* label */, value);
412         } catch (JSONException e) {
413             // Not an error; just a lookup key that doesn't have the right information.
414         }
415     }
416 }
417