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.ComponentName;
19 import android.content.Intent;
20 import android.content.Loader;
21 import android.database.Cursor;
22 import android.os.Bundle;
23 import android.support.annotation.MainThread;
24 import android.support.annotation.Nullable;
25 import android.text.TextUtils;
26 import android.util.ArraySet;
27 import android.view.LayoutInflater;
28 import android.view.MenuItem;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import com.android.contacts.common.R;
32 import com.android.contacts.common.util.AccountFilterUtil;
33 import com.android.dialer.callcomposer.CallComposerContact;
34 import com.android.dialer.callintent.CallInitiationType;
35 import com.android.dialer.callintent.CallInitiationType.Type;
36 import com.android.dialer.callintent.CallSpecificAppData;
37 import com.android.dialer.common.Assert;
38 import com.android.dialer.common.LogUtil;
39 import com.android.dialer.enrichedcall.EnrichedCallComponent;
40 import com.android.dialer.enrichedcall.EnrichedCallManager;
41 import com.android.dialer.logging.Logger;
42 import com.android.dialer.protos.ProtoParsers;
43 import java.util.Set;
44 import org.json.JSONException;
45 import org.json.JSONObject;
46 
47 /** Fragment containing a phone number list for picking. */
48 public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
49     implements PhoneNumberListAdapter.Listener, EnrichedCallManager.CapabilitiesListener {
50 
51   private static final String KEY_FILTER = "filter";
52   private OnPhoneNumberPickerActionListener mListener;
53   private ContactListFilter mFilter;
54   private View mAccountFilterHeader;
55   /**
56    * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set to View.GONE.
57    */
58   private View mPaddingView;
59   /** true if the loader has started at least once. */
60   private boolean mLoaderStarted;
61 
62   private boolean mUseCallableUri;
63 
64   private ContactListItemView.PhotoPosition mPhotoPosition =
65       ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */);
66 
67   private final Set<OnLoadFinishedListener> mLoadFinishedListeners = new ArraySet<>();
68 
69   private CursorReranker mCursorReranker;
70 
PhoneNumberPickerFragment()71   public PhoneNumberPickerFragment() {
72     setQuickContactEnabled(false);
73     setPhotoLoaderEnabled(true);
74     setSectionHeaderDisplayEnabled(true);
75     setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
76 
77     // Show nothing instead of letting caller Activity show something.
78     setHasOptionsMenu(true);
79   }
80 
81   /**
82    * Handles a click on the video call icon for a row in the list.
83    *
84    * @param position The position in the list where the click ocurred.
85    */
86   @Override
onVideoCallIconClicked(int position)87   public void onVideoCallIconClicked(int position) {
88     callNumber(position, true /* isVideoCall */);
89   }
90 
91   @Override
onCallAndShareIconClicked(int position)92   public void onCallAndShareIconClicked(int position) {
93     // Required because of cyclic dependencies of everything depending on contacts/common.
94     String componentName = "com.android.dialer.callcomposer.CallComposerActivity";
95     Intent intent = new Intent();
96     intent.setComponent(new ComponentName(getContext(), componentName));
97     CallComposerContact contact =
98         ((PhoneNumberListAdapter) getAdapter()).getCallComposerContact(position);
99     ProtoParsers.put(intent, "CALL_COMPOSER_CONTACT", contact);
100     startActivity(intent);
101   }
102 
setDirectorySearchEnabled(boolean flag)103   public void setDirectorySearchEnabled(boolean flag) {
104     setDirectorySearchMode(
105         flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : 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     updateFilterHeaderView();
126 
127     setVisibleScrollbarEnabled(getVisibleScrollbarEnabled());
128   }
129 
130   @Override
onPause()131   public void onPause() {
132     super.onPause();
133     EnrichedCallComponent.get(getContext())
134         .getEnrichedCallManager()
135         .unregisterCapabilitiesListener(this);
136   }
137 
138   @Override
onResume()139   public void onResume() {
140     super.onResume();
141     EnrichedCallComponent.get(getContext())
142         .getEnrichedCallManager()
143         .registerCapabilitiesListener(this);
144   }
145 
getVisibleScrollbarEnabled()146   protected boolean getVisibleScrollbarEnabled() {
147     return true;
148   }
149 
150   @Override
setSearchMode(boolean flag)151   protected void setSearchMode(boolean flag) {
152     super.setSearchMode(flag);
153     updateFilterHeaderView();
154   }
155 
updateFilterHeaderView()156   private void updateFilterHeaderView() {
157     final ContactListFilter filter = getFilter();
158     if (mAccountFilterHeader == null || filter == null) {
159       return;
160     }
161     final boolean shouldShowHeader =
162         !isSearchMode()
163             && AccountFilterUtil.updateAccountFilterTitleForPhone(
164                 mAccountFilterHeader, filter, false);
165     if (shouldShowHeader) {
166       mPaddingView.setVisibility(View.GONE);
167       mAccountFilterHeader.setVisibility(View.VISIBLE);
168     } else {
169       mPaddingView.setVisibility(View.VISIBLE);
170       mAccountFilterHeader.setVisibility(View.GONE);
171     }
172   }
173 
174   @Override
restoreSavedState(Bundle savedState)175   public void restoreSavedState(Bundle savedState) {
176     super.restoreSavedState(savedState);
177 
178     if (savedState == null) {
179       return;
180     }
181 
182     mFilter = savedState.getParcelable(KEY_FILTER);
183   }
184 
185   @Override
onSaveInstanceState(Bundle outState)186   public void onSaveInstanceState(Bundle outState) {
187     super.onSaveInstanceState(outState);
188     outState.putParcelable(KEY_FILTER, mFilter);
189   }
190 
191   @Override
onOptionsItemSelected(MenuItem item)192   public boolean onOptionsItemSelected(MenuItem item) {
193     final int itemId = item.getItemId();
194     if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
195       if (mListener != null) {
196         mListener.onHomeInActionBarSelected();
197       }
198       return true;
199     }
200     return super.onOptionsItemSelected(item);
201   }
202 
203   @Override
onItemClick(int position, long id)204   protected void onItemClick(int position, long id) {
205     callNumber(position, false /* isVideoCall */);
206   }
207 
208   /**
209    * Initiates a call to the number at the specified position.
210    *
211    * @param position The position.
212    * @param isVideoCall {@code true} if the call should be initiated as a video call, {@code false}
213    *     otherwise.
214    */
callNumber(int position, boolean isVideoCall)215   private void callNumber(int position, boolean isVideoCall) {
216     final String number = getPhoneNumber(position);
217     if (!TextUtils.isEmpty(number)) {
218       cacheContactInfo(position);
219       CallSpecificAppData callSpecificAppData =
220           CallSpecificAppData.newBuilder()
221               .setCallInitiationType(getCallInitiationType(true /* isRemoteDirectory */))
222               .setPositionOfSelectedSearchResult(position)
223               .setCharactersInSearchString(getQueryString() == null ? 0 : getQueryString().length())
224               .build();
225       mListener.onPickPhoneNumber(number, isVideoCall, callSpecificAppData);
226     } else {
227       LogUtil.i(
228           "PhoneNumberPickerFragment.callNumber",
229           "item at %d was clicked before adapter is ready, ignoring",
230           position);
231     }
232 
233     // Get the lookup key and track any analytics
234     final String lookupKey = getLookupKey(position);
235     if (!TextUtils.isEmpty(lookupKey)) {
236       maybeTrackAnalytics(lookupKey);
237     }
238   }
239 
cacheContactInfo(int position)240   protected void cacheContactInfo(int position) {
241     // Not implemented. Hook for child classes
242   }
243 
getPhoneNumber(int position)244   protected String getPhoneNumber(int position) {
245     final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
246     return adapter.getPhoneNumber(position);
247   }
248 
getLookupKey(int position)249   protected String getLookupKey(int position) {
250     final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
251     return adapter.getLookupKey(position);
252   }
253 
254   @Override
startLoading()255   protected void startLoading() {
256     mLoaderStarted = true;
257     super.startLoading();
258   }
259 
260   @Override
261   @MainThread
onLoadFinished(Loader<Cursor> loader, Cursor data)262   public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
263     Assert.isMainThread();
264     // TODO: define and verify behavior for "Nearby places", corp directories,
265     // and dividers listed in UI between these categories
266     if (mCursorReranker != null
267         && data != null
268         && !data.isClosed()
269         && data.getCount() > 0
270         && loader.getId() != -1) { // skip invalid directory ID of -1
271       data = mCursorReranker.rerankCursor(data);
272     }
273     super.onLoadFinished(loader, data);
274 
275     // disable scroll bar if there is no data
276     setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0);
277 
278     if (data != null) {
279       notifyListeners();
280     }
281   }
282 
283   /** Ranks cursor data rows and returns reference to new cursor object with reordered data. */
284   public interface CursorReranker {
285     @MainThread
rerankCursor(Cursor data)286     Cursor rerankCursor(Cursor data);
287   }
288 
289   @MainThread
setReranker(@ullable CursorReranker reranker)290   public void setReranker(@Nullable CursorReranker reranker) {
291     Assert.isMainThread();
292     mCursorReranker = reranker;
293   }
294 
295   /** Listener that is notified when cursor has finished loading data. */
296   public interface OnLoadFinishedListener {
onLoadFinished()297     void onLoadFinished();
298   }
299 
300   @MainThread
addOnLoadFinishedListener(OnLoadFinishedListener listener)301   public void addOnLoadFinishedListener(OnLoadFinishedListener listener) {
302     Assert.isMainThread();
303     mLoadFinishedListeners.add(listener);
304   }
305 
306   @MainThread
removeOnLoadFinishedListener(OnLoadFinishedListener listener)307   public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) {
308     Assert.isMainThread();
309     mLoadFinishedListeners.remove(listener);
310   }
311 
312   @MainThread
notifyListeners()313   protected void notifyListeners() {
314     Assert.isMainThread();
315     for (OnLoadFinishedListener listener : mLoadFinishedListeners) {
316       listener.onLoadFinished();
317     }
318   }
319 
320   @Override
onCapabilitiesUpdated()321   public void onCapabilitiesUpdated() {
322     if (getAdapter() != null) {
323       getAdapter().notifyDataSetChanged();
324     }
325   }
326 
327   @MainThread
328   @Override
onDetach()329   public void onDetach() {
330     Assert.isMainThread();
331     mLoadFinishedListeners.clear();
332     super.onDetach();
333   }
334 
setUseCallableUri(boolean useCallableUri)335   public void setUseCallableUri(boolean useCallableUri) {
336     mUseCallableUri = useCallableUri;
337   }
338 
usesCallableUri()339   public boolean usesCallableUri() {
340     return mUseCallableUri;
341   }
342 
343   @Override
createListAdapter()344   protected ContactEntryListAdapter createListAdapter() {
345     PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
346     adapter.setDisplayPhotos(true);
347     adapter.setUseCallableUri(mUseCallableUri);
348     return adapter;
349   }
350 
351   @Override
configureAdapter()352   protected void configureAdapter() {
353     super.configureAdapter();
354 
355     final ContactEntryListAdapter adapter = getAdapter();
356     if (adapter == null) {
357       return;
358     }
359 
360     if (!isSearchMode() && mFilter != null) {
361       adapter.setFilter(mFilter);
362     }
363 
364     setPhotoPosition(adapter);
365   }
366 
setPhotoPosition(ContactEntryListAdapter adapter)367   protected void setPhotoPosition(ContactEntryListAdapter adapter) {
368     ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition);
369   }
370 
371   @Override
inflateView(LayoutInflater inflater, ViewGroup container)372   protected View inflateView(LayoutInflater inflater, ViewGroup container) {
373     return inflater.inflate(R.layout.contact_list_content, null);
374   }
375 
getFilter()376   public ContactListFilter getFilter() {
377     return mFilter;
378   }
379 
setFilter(ContactListFilter filter)380   public void setFilter(ContactListFilter filter) {
381     if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) {
382       return;
383     }
384 
385     mFilter = filter;
386     if (mLoaderStarted) {
387       reloadData();
388     }
389     updateFilterHeaderView();
390   }
391 
setPhotoPosition(ContactListItemView.PhotoPosition photoPosition)392   public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
393     mPhotoPosition = photoPosition;
394 
395     final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
396     if (adapter != null) {
397       adapter.setPhotoPosition(photoPosition);
398     }
399   }
400 
401   /**
402    * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number
403    *     not in the local contacts database
404    */
getCallInitiationType(boolean isRemoteDirectory)405   protected CallInitiationType.Type getCallInitiationType(boolean isRemoteDirectory) {
406     return Type.UNKNOWN_INITIATION;
407   }
408 
409   /**
410    * Where a lookup key contains analytic event information, logs the associated analytics event.
411    *
412    * @param lookupKey The lookup key JSON object.
413    */
maybeTrackAnalytics(String lookupKey)414   private void maybeTrackAnalytics(String lookupKey) {
415     try {
416       JSONObject json = new JSONObject(lookupKey);
417 
418       String analyticsCategory =
419           json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY);
420       String analyticsAction = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION);
421       String analyticsValue = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE);
422 
423       if (TextUtils.isEmpty(analyticsCategory)
424           || TextUtils.isEmpty(analyticsAction)
425           || TextUtils.isEmpty(analyticsValue)) {
426         return;
427       }
428 
429       // Assume that the analytic value being tracked could be a float value, but just cast
430       // to a long so that the analytic server can handle it.
431       long value;
432       try {
433         float floatValue = Float.parseFloat(analyticsValue);
434         value = (long) floatValue;
435       } catch (NumberFormatException nfe) {
436         return;
437       }
438 
439       Logger.get(getActivity())
440           .sendHitEventAnalytics(analyticsCategory, analyticsAction, "" /* label */, value);
441     } catch (JSONException e) {
442       // Not an error; just a lookup key that doesn't have the right information.
443     }
444   }
445 }
446