• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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