Home | History | Annotate | Download | only in list
      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 
     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
     87   public void onVideoCallIconClicked(int position) {
     88     callNumber(position, true /* isVideoCall */);
     89   }
     90 
     91   @Override
     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 
    103   public void setDirectorySearchEnabled(boolean flag) {
    104     setDirectorySearchMode(
    105         flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : DirectoryListLoader.SEARCH_MODE_NONE);
    106   }
    107 
    108   public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) {
    109     this.mListener = listener;
    110   }
    111 
    112   public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() {
    113     return mListener;
    114   }
    115 
    116   @Override
    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
    131   public void onPause() {
    132     super.onPause();
    133     EnrichedCallComponent.get(getContext())
    134         .getEnrichedCallManager()
    135         .unregisterCapabilitiesListener(this);
    136   }
    137 
    138   @Override
    139   public void onResume() {
    140     super.onResume();
    141     EnrichedCallComponent.get(getContext())
    142         .getEnrichedCallManager()
    143         .registerCapabilitiesListener(this);
    144   }
    145 
    146   protected boolean getVisibleScrollbarEnabled() {
    147     return true;
    148   }
    149 
    150   @Override
    151   protected void setSearchMode(boolean flag) {
    152     super.setSearchMode(flag);
    153     updateFilterHeaderView();
    154   }
    155 
    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
    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
    186   public void onSaveInstanceState(Bundle outState) {
    187     super.onSaveInstanceState(outState);
    188     outState.putParcelable(KEY_FILTER, mFilter);
    189   }
    190 
    191   @Override
    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
    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    */
    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 
    240   protected void cacheContactInfo(int position) {
    241     // Not implemented. Hook for child classes
    242   }
    243 
    244   protected String getPhoneNumber(int position) {
    245     final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
    246     return adapter.getPhoneNumber(position);
    247   }
    248 
    249   protected String getLookupKey(int position) {
    250     final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
    251     return adapter.getLookupKey(position);
    252   }
    253 
    254   @Override
    255   protected void startLoading() {
    256     mLoaderStarted = true;
    257     super.startLoading();
    258   }
    259 
    260   @Override
    261   @MainThread
    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
    286     Cursor rerankCursor(Cursor data);
    287   }
    288 
    289   @MainThread
    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 {
    297     void onLoadFinished();
    298   }
    299 
    300   @MainThread
    301   public void addOnLoadFinishedListener(OnLoadFinishedListener listener) {
    302     Assert.isMainThread();
    303     mLoadFinishedListeners.add(listener);
    304   }
    305 
    306   @MainThread
    307   public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) {
    308     Assert.isMainThread();
    309     mLoadFinishedListeners.remove(listener);
    310   }
    311 
    312   @MainThread
    313   protected void notifyListeners() {
    314     Assert.isMainThread();
    315     for (OnLoadFinishedListener listener : mLoadFinishedListeners) {
    316       listener.onLoadFinished();
    317     }
    318   }
    319 
    320   @Override
    321   public void onCapabilitiesUpdated() {
    322     if (getAdapter() != null) {
    323       getAdapter().notifyDataSetChanged();
    324     }
    325   }
    326 
    327   @MainThread
    328   @Override
    329   public void onDetach() {
    330     Assert.isMainThread();
    331     mLoadFinishedListeners.clear();
    332     super.onDetach();
    333   }
    334 
    335   public void setUseCallableUri(boolean useCallableUri) {
    336     mUseCallableUri = useCallableUri;
    337   }
    338 
    339   public boolean usesCallableUri() {
    340     return mUseCallableUri;
    341   }
    342 
    343   @Override
    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
    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 
    367   protected void setPhotoPosition(ContactEntryListAdapter adapter) {
    368     ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition);
    369   }
    370 
    371   @Override
    372   protected View inflateView(LayoutInflater inflater, ViewGroup container) {
    373     return inflater.inflate(R.layout.contact_list_content, null);
    374   }
    375 
    376   public ContactListFilter getFilter() {
    377     return mFilter;
    378   }
    379 
    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 
    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    */
    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    */
    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