/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts.common.list; import android.content.ComponentName; import android.content.Intent; import android.content.Loader; import android.database.Cursor; import android.os.Bundle; import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.ArraySet; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import com.android.contacts.common.R; import com.android.contacts.common.util.AccountFilterUtil; import com.android.dialer.callcomposer.CallComposerContact; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallInitiationType.Type; import com.android.dialer.callintent.CallSpecificAppData; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.logging.Logger; import com.android.dialer.protos.ProtoParsers; import java.util.Set; import org.json.JSONException; import org.json.JSONObject; /** Fragment containing a phone number list for picking. */ public class PhoneNumberPickerFragment extends ContactEntryListFragment implements PhoneNumberListAdapter.Listener, EnrichedCallManager.CapabilitiesListener { private static final String KEY_FILTER = "filter"; private OnPhoneNumberPickerActionListener mListener; private ContactListFilter mFilter; private View mAccountFilterHeader; /** * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set to View.GONE. */ private View mPaddingView; /** true if the loader has started at least once. */ private boolean mLoaderStarted; private boolean mUseCallableUri; private ContactListItemView.PhotoPosition mPhotoPosition = ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */); private final Set mLoadFinishedListeners = new ArraySet<>(); private CursorReranker mCursorReranker; public PhoneNumberPickerFragment() { setQuickContactEnabled(false); setPhotoLoaderEnabled(true); setSectionHeaderDisplayEnabled(true); setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); // Show nothing instead of letting caller Activity show something. setHasOptionsMenu(true); } /** * Handles a click on the video call icon for a row in the list. * * @param position The position in the list where the click ocurred. */ @Override public void onVideoCallIconClicked(int position) { callNumber(position, true /* isVideoCall */); } @Override public void onCallAndShareIconClicked(int position) { // Required because of cyclic dependencies of everything depending on contacts/common. String componentName = "com.android.dialer.callcomposer.CallComposerActivity"; Intent intent = new Intent(); intent.setComponent(new ComponentName(getContext(), componentName)); CallComposerContact contact = ((PhoneNumberListAdapter) getAdapter()).getCallComposerContact(position); ProtoParsers.put(intent, "CALL_COMPOSER_CONTACT", contact); startActivity(intent); } public void setDirectorySearchEnabled(boolean flag) { setDirectorySearchMode( flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : DirectoryListLoader.SEARCH_MODE_NONE); } public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) { this.mListener = listener; } public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() { return mListener; } @Override protected void onCreateView(LayoutInflater inflater, ViewGroup container) { super.onCreateView(inflater, container); View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false); mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding); getListView().addHeaderView(paddingView); mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container); updateFilterHeaderView(); setVisibleScrollbarEnabled(getVisibleScrollbarEnabled()); } @Override public void onPause() { super.onPause(); EnrichedCallComponent.get(getContext()) .getEnrichedCallManager() .unregisterCapabilitiesListener(this); } @Override public void onResume() { super.onResume(); EnrichedCallComponent.get(getContext()) .getEnrichedCallManager() .registerCapabilitiesListener(this); } protected boolean getVisibleScrollbarEnabled() { return true; } @Override protected void setSearchMode(boolean flag) { super.setSearchMode(flag); updateFilterHeaderView(); } private void updateFilterHeaderView() { final ContactListFilter filter = getFilter(); if (mAccountFilterHeader == null || filter == null) { return; } final boolean shouldShowHeader = !isSearchMode() && AccountFilterUtil.updateAccountFilterTitleForPhone( mAccountFilterHeader, filter, false); if (shouldShowHeader) { mPaddingView.setVisibility(View.GONE); mAccountFilterHeader.setVisibility(View.VISIBLE); } else { mPaddingView.setVisibility(View.VISIBLE); mAccountFilterHeader.setVisibility(View.GONE); } } @Override public void restoreSavedState(Bundle savedState) { super.restoreSavedState(savedState); if (savedState == null) { return; } mFilter = savedState.getParcelable(KEY_FILTER); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(KEY_FILTER, mFilter); } @Override public boolean onOptionsItemSelected(MenuItem item) { final int itemId = item.getItemId(); if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled() if (mListener != null) { mListener.onHomeInActionBarSelected(); } return true; } return super.onOptionsItemSelected(item); } @Override protected void onItemClick(int position, long id) { callNumber(position, false /* isVideoCall */); } /** * Initiates a call to the number at the specified position. * * @param position The position. * @param isVideoCall {@code true} if the call should be initiated as a video call, {@code false} * otherwise. */ private void callNumber(int position, boolean isVideoCall) { final String number = getPhoneNumber(position); if (!TextUtils.isEmpty(number)) { cacheContactInfo(position); CallSpecificAppData callSpecificAppData = CallSpecificAppData.newBuilder() .setCallInitiationType(getCallInitiationType(true /* isRemoteDirectory */)) .setPositionOfSelectedSearchResult(position) .setCharactersInSearchString(getQueryString() == null ? 0 : getQueryString().length()) .build(); mListener.onPickPhoneNumber(number, isVideoCall, callSpecificAppData); } else { LogUtil.i( "PhoneNumberPickerFragment.callNumber", "item at %d was clicked before adapter is ready, ignoring", position); } // Get the lookup key and track any analytics final String lookupKey = getLookupKey(position); if (!TextUtils.isEmpty(lookupKey)) { maybeTrackAnalytics(lookupKey); } } protected void cacheContactInfo(int position) { // Not implemented. Hook for child classes } protected String getPhoneNumber(int position) { final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); return adapter.getPhoneNumber(position); } protected String getLookupKey(int position) { final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); return adapter.getLookupKey(position); } @Override protected void startLoading() { mLoaderStarted = true; super.startLoading(); } @Override @MainThread public void onLoadFinished(Loader loader, Cursor data) { Assert.isMainThread(); // TODO: define and verify behavior for "Nearby places", corp directories, // and dividers listed in UI between these categories if (mCursorReranker != null && data != null && !data.isClosed() && data.getCount() > 0 && loader.getId() != -1) { // skip invalid directory ID of -1 data = mCursorReranker.rerankCursor(data); } super.onLoadFinished(loader, data); // disable scroll bar if there is no data setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0); if (data != null) { notifyListeners(); } } /** Ranks cursor data rows and returns reference to new cursor object with reordered data. */ public interface CursorReranker { @MainThread Cursor rerankCursor(Cursor data); } @MainThread public void setReranker(@Nullable CursorReranker reranker) { Assert.isMainThread(); mCursorReranker = reranker; } /** Listener that is notified when cursor has finished loading data. */ public interface OnLoadFinishedListener { void onLoadFinished(); } @MainThread public void addOnLoadFinishedListener(OnLoadFinishedListener listener) { Assert.isMainThread(); mLoadFinishedListeners.add(listener); } @MainThread public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) { Assert.isMainThread(); mLoadFinishedListeners.remove(listener); } @MainThread protected void notifyListeners() { Assert.isMainThread(); for (OnLoadFinishedListener listener : mLoadFinishedListeners) { listener.onLoadFinished(); } } @Override public void onCapabilitiesUpdated() { if (getAdapter() != null) { getAdapter().notifyDataSetChanged(); } } @MainThread @Override public void onDetach() { Assert.isMainThread(); mLoadFinishedListeners.clear(); super.onDetach(); } public void setUseCallableUri(boolean useCallableUri) { mUseCallableUri = useCallableUri; } public boolean usesCallableUri() { return mUseCallableUri; } @Override protected ContactEntryListAdapter createListAdapter() { PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity()); adapter.setDisplayPhotos(true); adapter.setUseCallableUri(mUseCallableUri); return adapter; } @Override protected void configureAdapter() { super.configureAdapter(); final ContactEntryListAdapter adapter = getAdapter(); if (adapter == null) { return; } if (!isSearchMode() && mFilter != null) { adapter.setFilter(mFilter); } setPhotoPosition(adapter); } protected void setPhotoPosition(ContactEntryListAdapter adapter) { ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition); } @Override protected View inflateView(LayoutInflater inflater, ViewGroup container) { return inflater.inflate(R.layout.contact_list_content, null); } public ContactListFilter getFilter() { return mFilter; } public void setFilter(ContactListFilter filter) { if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) { return; } mFilter = filter; if (mLoaderStarted) { reloadData(); } updateFilterHeaderView(); } public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { mPhotoPosition = photoPosition; final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); if (adapter != null) { adapter.setPhotoPosition(photoPosition); } } /** * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number * not in the local contacts database */ protected CallInitiationType.Type getCallInitiationType(boolean isRemoteDirectory) { return Type.UNKNOWN_INITIATION; } /** * Where a lookup key contains analytic event information, logs the associated analytics event. * * @param lookupKey The lookup key JSON object. */ private void maybeTrackAnalytics(String lookupKey) { try { JSONObject json = new JSONObject(lookupKey); String analyticsCategory = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY); String analyticsAction = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION); String analyticsValue = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE); if (TextUtils.isEmpty(analyticsCategory) || TextUtils.isEmpty(analyticsAction) || TextUtils.isEmpty(analyticsValue)) { return; } // Assume that the analytic value being tracked could be a float value, but just cast // to a long so that the analytic server can handle it. long value; try { float floatValue = Float.parseFloat(analyticsValue); value = (long) floatValue; } catch (NumberFormatException nfe) { return; } Logger.get(getActivity()) .sendHitEventAnalytics(analyticsCategory, analyticsAction, "" /* label */, value); } catch (JSONException e) { // Not an error; just a lookup key that doesn't have the right information. } } }