1 /* 2 * Copyright (C) 2017 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 17 package com.android.dialer.speeddial; 18 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.app.DialogFragment; 22 import android.app.FragmentManager; 23 import android.content.ContentResolver; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.os.Bundle; 27 import android.provider.ContactsContract.CommonDataKinds.Phone; 28 import android.support.annotation.Nullable; 29 import android.support.annotation.VisibleForTesting; 30 import android.text.TextUtils; 31 import android.util.ArraySet; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.widget.LinearLayout; 35 import android.widget.TextView; 36 import com.android.dialer.callintent.CallInitiationType; 37 import com.android.dialer.callintent.CallIntentBuilder; 38 import com.android.dialer.common.LogUtil; 39 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 40 import com.android.dialer.common.concurrent.DialerExecutorComponent; 41 import com.android.dialer.duo.DuoComponent; 42 import com.android.dialer.precall.PreCall; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.Set; 46 47 /** Disambiguation dialog for favorite contacts in {@link SpeedDialFragment}. */ 48 public class DisambigDialog extends DialogFragment { 49 50 @VisibleForTesting public static final String DISAMBIG_DIALOG_TAG = "disambig_dialog"; 51 private static final String DISAMBIG_DIALOG_WORKER_TAG = "disambig_dialog_worker"; 52 53 private final Set<String> phoneNumbers = new ArraySet<>(); 54 private LinearLayout container; 55 private String lookupKey; 56 57 /** Show a disambiguation dialog for a starred contact without a favorite communication avenue. */ show(String lookupKey, FragmentManager manager)58 public static DisambigDialog show(String lookupKey, FragmentManager manager) { 59 DisambigDialog dialog = new DisambigDialog(); 60 dialog.lookupKey = lookupKey; 61 dialog.show(manager, DISAMBIG_DIALOG_TAG); 62 return dialog; 63 } 64 65 @Override onCreateDialog(Bundle savedInstanceState)66 public Dialog onCreateDialog(Bundle savedInstanceState) { 67 LayoutInflater inflater = getActivity().getLayoutInflater(); 68 View view = inflater.inflate(R.layout.disambig_dialog_layout, null, false); 69 container = view.findViewById(R.id.communication_avenue_container); 70 return new AlertDialog.Builder(getActivity()).setView(view).create(); 71 } 72 73 @Override onResume()74 public void onResume() { 75 super.onResume(); 76 lookupContactInfo(); 77 } 78 79 @Override onPause()80 public void onPause() { 81 super.onPause(); 82 // TODO(calderwoodra): for simplicity, just dismiss the dialog on configuration change and 83 // consider changing this later. 84 dismiss(); 85 } 86 lookupContactInfo()87 private void lookupContactInfo() { 88 DialerExecutorComponent.get(getContext()) 89 .dialerExecutorFactory() 90 .createUiTaskBuilder( 91 getFragmentManager(), 92 DISAMBIG_DIALOG_WORKER_TAG, 93 new LookupContactInfoWorker(getContext().getContentResolver())) 94 .onSuccess(this::insertOptions) 95 .onFailure(this::onLookupFailed) 96 .build() 97 .executeParallel(lookupKey); 98 } 99 100 /** 101 * Inflates and inserts the following in the dialog: 102 * 103 * <ul> 104 * <li>Header for each unique phone number 105 * <li>Clickable video option if the phone number is video reachable (ViLTE, Duo) 106 * <li>Clickable voice option 107 * </ul> 108 */ insertOptions(Cursor cursor)109 private void insertOptions(Cursor cursor) { 110 if (!cursorIsValid(cursor)) { 111 dismiss(); 112 return; 113 } 114 115 do { 116 String number = cursor.getString(LookupContactInfoWorker.NUMBER_INDEX); 117 // TODO(calderwoodra): improve this to include fuzzy matching 118 if (phoneNumbers.add(number)) { 119 insertOption( 120 number, 121 getLabel(getContext().getResources(), cursor), 122 isVideoReachable(cursor, number)); 123 } 124 } while (cursor.moveToNext()); 125 cursor.close(); 126 // TODO(calderwoodra): set max height of the scrollview. Might need to override onMeasure. 127 } 128 129 /** Returns true if the given number is ViLTE reachable or Duo reachable. */ isVideoReachable(Cursor cursor, String number)130 private boolean isVideoReachable(Cursor cursor, String number) { 131 boolean isVideoReachable = cursor.getInt(LookupContactInfoWorker.PHONE_PRESENCE_INDEX) == 1; 132 if (!isVideoReachable) { 133 isVideoReachable = DuoComponent.get(getContext()).getDuo().isReachable(getContext(), number); 134 } 135 return isVideoReachable; 136 } 137 138 /** Inserts a group of options for a specific phone number. */ insertOption(String number, String phoneType, boolean isVideoReachable)139 private void insertOption(String number, String phoneType, boolean isVideoReachable) { 140 View view = 141 getActivity() 142 .getLayoutInflater() 143 .inflate(R.layout.disambig_option_layout, container, false); 144 ((TextView) view.findViewById(R.id.phone_type)).setText(phoneType); 145 ((TextView) view.findViewById(R.id.phone_number)).setText(number); 146 147 if (isVideoReachable) { 148 View videoOption = view.findViewById(R.id.video_call_container); 149 videoOption.setOnClickListener(v -> onVideoOptionClicked(number)); 150 videoOption.setVisibility(View.VISIBLE); 151 } 152 View voiceOption = view.findViewById(R.id.voice_call_container); 153 voiceOption.setOnClickListener(v -> onVoiceOptionClicked(number)); 154 container.addView(view); 155 } 156 onVideoOptionClicked(String number)157 private void onVideoOptionClicked(String number) { 158 // TODO(calderwoodra): save this option if remember is checked 159 // TODO(calderwoodra): place a duo call if possible 160 PreCall.start( 161 getContext(), 162 new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL).setIsVideoCall(true)); 163 } 164 onVoiceOptionClicked(String number)165 private void onVoiceOptionClicked(String number) { 166 // TODO(calderwoodra): save this option if remember is checked 167 PreCall.start(getContext(), new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL)); 168 } 169 170 // TODO(calderwoodra): handle CNAP and cequint types. 171 // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType getLabel(Resources resources, Cursor cursor)172 private static String getLabel(Resources resources, Cursor cursor) { 173 int numberType = cursor.getInt(LookupContactInfoWorker.PHONE_TYPE_INDEX); 174 String numberLabel = cursor.getString(LookupContactInfoWorker.PHONE_LABEL_INDEX); 175 176 // Returns empty label instead of "custom" if the custom label is empty. 177 if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) { 178 return ""; 179 } 180 return (String) Phone.getTypeLabel(resources, numberType, numberLabel); 181 } 182 183 // Checks if the cursor is valid and logs an error if there are any issues. cursorIsValid(Cursor cursor)184 private static boolean cursorIsValid(Cursor cursor) { 185 if (cursor == null) { 186 LogUtil.e("DisambigDialog.insertOptions", "cursor null."); 187 return false; 188 } else if (cursor.isClosed()) { 189 LogUtil.e("DisambigDialog.insertOptions", "cursor closed."); 190 cursor.close(); 191 return false; 192 } else if (!cursor.moveToFirst()) { 193 LogUtil.e("DisambigDialog.insertOptions", "cursor empty."); 194 cursor.close(); 195 return false; 196 } 197 return true; 198 } 199 onLookupFailed(Throwable throwable)200 private void onLookupFailed(Throwable throwable) { 201 LogUtil.e("DisambigDialog.onLookupFailed", null, throwable); 202 insertOptions(null); 203 } 204 205 private static class LookupContactInfoWorker implements Worker<String, Cursor> { 206 207 static final int NUMBER_INDEX = 0; 208 static final int PHONE_TYPE_INDEX = 1; 209 static final int PHONE_LABEL_INDEX = 2; 210 static final int PHONE_PRESENCE_INDEX = 3; 211 212 private static final String[] projection = 213 new String[] {Phone.NUMBER, Phone.TYPE, Phone.LABEL, Phone.CARRIER_PRESENCE}; 214 private final ContentResolver resolver; 215 LookupContactInfoWorker(ContentResolver resolver)216 LookupContactInfoWorker(ContentResolver resolver) { 217 this.resolver = resolver; 218 } 219 220 @Nullable 221 @Override doInBackground(@ullable String lookupKey)222 public Cursor doInBackground(@Nullable String lookupKey) throws Throwable { 223 if (TextUtils.isEmpty(lookupKey)) { 224 LogUtil.e("LookupConctactInfoWorker.doInBackground", "contact id unsest."); 225 return null; 226 } 227 return resolver.query( 228 Phone.CONTENT_URI, projection, Phone.LOOKUP_KEY + " = ?", new String[] {lookupKey}, null); 229 } 230 } 231 232 @VisibleForTesting getProjectionForTesting()233 public static String[] getProjectionForTesting() { 234 ArrayList<String> projection = 235 new ArrayList<>(Arrays.asList(LookupContactInfoWorker.projection)); 236 projection.add(Phone.LOOKUP_KEY); 237 return projection.toArray(new String[projection.size()]); 238 } 239 240 @VisibleForTesting getContainer()241 public LinearLayout getContainer() { 242 return container; 243 } 244 } 245