• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.preferredsim.impl;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.provider.ContactsContract.Contacts;
28 import android.provider.ContactsContract.Data;
29 import android.provider.ContactsContract.PhoneLookup;
30 import android.provider.ContactsContract.QuickContact;
31 import android.provider.ContactsContract.RawContacts;
32 import android.support.annotation.NonNull;
33 import android.support.annotation.Nullable;
34 import android.support.annotation.VisibleForTesting;
35 import android.support.annotation.WorkerThread;
36 import android.telecom.PhoneAccount;
37 import android.telecom.PhoneAccountHandle;
38 import android.telecom.TelecomManager;
39 import android.text.TextUtils;
40 import com.android.contacts.common.widget.SelectPhoneAccountDialogOptions;
41 import com.android.contacts.common.widget.SelectPhoneAccountDialogOptionsUtil;
42 import com.android.dialer.activecalls.ActiveCallInfo;
43 import com.android.dialer.activecalls.ActiveCallsComponent;
44 import com.android.dialer.common.Assert;
45 import com.android.dialer.common.LogUtil;
46 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
47 import com.android.dialer.configprovider.ConfigProviderComponent;
48 import com.android.dialer.inject.ApplicationContext;
49 import com.android.dialer.logging.DialerImpression.Type;
50 import com.android.dialer.logging.Logger;
51 import com.android.dialer.preferredsim.PreferredAccountUtil;
52 import com.android.dialer.preferredsim.PreferredAccountWorker;
53 import com.android.dialer.preferredsim.PreferredAccountWorker.Result.Builder;
54 import com.android.dialer.preferredsim.PreferredSimFallbackContract;
55 import com.android.dialer.preferredsim.PreferredSimFallbackContract.PreferredSim;
56 import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent;
57 import com.android.dialer.preferredsim.suggestion.SuggestionProvider;
58 import com.android.dialer.preferredsim.suggestion.SuggestionProvider.Suggestion;
59 import com.android.dialer.util.PermissionsUtil;
60 import com.google.common.base.Optional;
61 import com.google.common.collect.ImmutableList;
62 import com.google.common.collect.ImmutableSet;
63 import com.google.common.util.concurrent.ListenableFuture;
64 import com.google.common.util.concurrent.ListeningExecutorService;
65 import java.util.List;
66 import java.util.Objects;
67 import javax.inject.Inject;
68 
69 /** Implements {@link PreferredAccountWorker}. */
70 @SuppressWarnings({"missingPermission", "Guava"})
71 public class PreferredAccountWorkerImpl implements PreferredAccountWorker {
72 
73   private final Context appContext;
74   private final ListeningExecutorService backgroundExecutor;
75 
76   @VisibleForTesting
77   public static final String METADATA_SUPPORTS_PREFERRED_SIM =
78       "supports_per_number_preferred_account";
79 
80   @Inject
PreferredAccountWorkerImpl( @pplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutor)81   public PreferredAccountWorkerImpl(
82       @ApplicationContext Context appContext,
83       @BackgroundExecutor ListeningExecutorService backgroundExecutor) {
84     this.appContext = appContext;
85     this.backgroundExecutor = backgroundExecutor;
86   }
87 
88   @Override
getVoicemailDialogOptions()89   public SelectPhoneAccountDialogOptions getVoicemailDialogOptions() {
90     return SelectPhoneAccountDialogOptionsUtil.builderWithAccounts(
91             appContext.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts())
92         .setTitle(R.string.pre_call_select_phone_account)
93         .setCanSetDefault(false)
94         .build();
95   }
96 
97   @Override
selectAccount( String phoneNumber, List<PhoneAccountHandle> candidates)98   public ListenableFuture<Result> selectAccount(
99       String phoneNumber, List<PhoneAccountHandle> candidates) {
100     return backgroundExecutor.submit(() -> doInBackground(phoneNumber, candidates));
101   }
102 
doInBackground(String phoneNumber, List<PhoneAccountHandle> candidates)103   private Result doInBackground(String phoneNumber, List<PhoneAccountHandle> candidates) {
104 
105     Optional<String> dataId = getDataId(phoneNumber);
106     if (dataId.isPresent()) {
107       Optional<PhoneAccountHandle> preferred = getPreferredAccount(appContext, dataId.get());
108       if (preferred.isPresent()) {
109         return usePreferredSim(preferred.get(), candidates, dataId.get());
110       }
111     }
112 
113     PhoneAccountHandle defaultPhoneAccount =
114         appContext
115             .getSystemService(TelecomManager.class)
116             .getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
117     if (defaultPhoneAccount != null) {
118       return useDefaultSim(defaultPhoneAccount, candidates, dataId.orNull());
119     }
120 
121     Optional<Suggestion> suggestion =
122         SimSuggestionComponent.get(appContext)
123             .getSuggestionProvider()
124             .getSuggestion(appContext, phoneNumber);
125     if (suggestion.isPresent() && suggestion.get().shouldAutoSelect) {
126       return useSuggestedSim(suggestion.get(), candidates, dataId.orNull());
127     }
128 
129     Builder resultBuilder =
130         Result.builder(
131             createDialogOptionsBuilder(candidates, dataId.orNull(), suggestion.orNull()));
132     if (suggestion.isPresent()) {
133       resultBuilder.setSuggestion(suggestion.get());
134     }
135     if (dataId.isPresent()) {
136       resultBuilder.setDataId(dataId.get());
137     }
138     return resultBuilder.build();
139   }
140 
usePreferredSim( PhoneAccountHandle preferred, List<PhoneAccountHandle> candidates, String dataId)141   private Result usePreferredSim(
142       PhoneAccountHandle preferred, List<PhoneAccountHandle> candidates, String dataId) {
143     Builder resultBuilder;
144     if (isSelectable(preferred)) {
145       Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_PREFERRED_USED);
146       resultBuilder = Result.builder(preferred);
147     } else {
148       Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_PREFERRED_NOT_SELECTABLE);
149       LogUtil.i("CallingAccountSelector.usePreferredAccount", "preferred account not selectable");
150       resultBuilder = Result.builder(createDialogOptionsBuilder(candidates, dataId, null));
151     }
152     resultBuilder.setDataId(dataId);
153     return resultBuilder.build();
154   }
155 
useDefaultSim( PhoneAccountHandle defaultPhoneAccount, List<PhoneAccountHandle> candidates, @Nullable String dataId)156   private Result useDefaultSim(
157       PhoneAccountHandle defaultPhoneAccount,
158       List<PhoneAccountHandle> candidates,
159       @Nullable String dataId) {
160     if (isSelectable(defaultPhoneAccount)) {
161       Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_GLOBAL_USED);
162       return Result.builder(defaultPhoneAccount).build();
163     } else {
164       Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_GLOBAL_NOT_SELECTABLE);
165       LogUtil.i("CallingAccountSelector.usePreferredAccount", "global account not selectable");
166       return Result.builder(createDialogOptionsBuilder(candidates, dataId, null)).build();
167     }
168   }
169 
useSuggestedSim( Suggestion suggestion, List<PhoneAccountHandle> candidates, @Nullable String dataId)170   private Result useSuggestedSim(
171       Suggestion suggestion, List<PhoneAccountHandle> candidates, @Nullable String dataId) {
172     Builder resultBuilder;
173     PhoneAccountHandle suggestedPhoneAccount = suggestion.phoneAccountHandle;
174     if (isSelectable(suggestedPhoneAccount)) {
175       resultBuilder = Result.builder(suggestedPhoneAccount);
176       Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AUTO_SELECTED);
177     } else {
178       Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AUTO_NOT_SELECTABLE);
179       LogUtil.i("CallingAccountSelector.usePreferredAccount", "global account not selectable");
180       resultBuilder = Result.builder(createDialogOptionsBuilder(candidates, dataId, suggestion));
181       return resultBuilder.build();
182     }
183     resultBuilder.setSuggestion(suggestion);
184     return resultBuilder.build();
185   }
186 
createDialogOptionsBuilder( List<PhoneAccountHandle> candidates, @Nullable String dataId, @Nullable Suggestion suggestion)187   SelectPhoneAccountDialogOptions.Builder createDialogOptionsBuilder(
188       List<PhoneAccountHandle> candidates,
189       @Nullable String dataId,
190       @Nullable Suggestion suggestion) {
191     Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SHOWN);
192     if (dataId != null) {
193       Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_IN_CONTACTS);
194     }
195     if (suggestion != null) {
196       Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTION_AVAILABLE);
197       switch (suggestion.reason) {
198         case INTRA_CARRIER:
199           Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTED_CARRIER);
200           break;
201         case FREQUENT:
202           Logger.get(appContext).logImpression(Type.DUAL_SIM_SELECTION_SUGGESTED_FREQUENCY);
203           break;
204         default:
205       }
206     }
207     SelectPhoneAccountDialogOptions.Builder optionsBuilder =
208         SelectPhoneAccountDialogOptions.newBuilder()
209             .setTitle(R.string.pre_call_select_phone_account)
210             .setCanSetDefault(dataId != null)
211             .setSetDefaultLabel(R.string.pre_call_select_phone_account_remember);
212 
213     for (PhoneAccountHandle phoneAccountHandle : candidates) {
214       SelectPhoneAccountDialogOptions.Entry.Builder entryBuilder =
215           SelectPhoneAccountDialogOptions.Entry.newBuilder();
216       SelectPhoneAccountDialogOptionsUtil.setPhoneAccountHandle(entryBuilder, phoneAccountHandle);
217       if (isSelectable(phoneAccountHandle)) {
218         Optional<String> hint =
219             SuggestionProvider.getHint(appContext, phoneAccountHandle, suggestion);
220         if (hint.isPresent()) {
221           entryBuilder.setHint(hint.get());
222         }
223       } else {
224         entryBuilder.setEnabled(false);
225         Optional<String> activeCallLabel = getActiveCallLabel();
226         if (activeCallLabel.isPresent()) {
227           entryBuilder.setHint(
228               appContext.getString(
229                   R.string.pre_call_select_phone_account_hint_other_sim_in_use,
230                   activeCallLabel.get()));
231         }
232       }
233       optionsBuilder.addEntries(entryBuilder);
234     }
235 
236     return optionsBuilder;
237   }
238 
239   @WorkerThread
240   @NonNull
getDataId(@ullable String phoneNumber)241   private Optional<String> getDataId(@Nullable String phoneNumber) {
242     Assert.isWorkerThread();
243 
244     if (!isPreferredSimEnabled(appContext)) {
245       return Optional.absent();
246     }
247     if (!PermissionsUtil.hasContactsReadPermissions(appContext)) {
248       LogUtil.i("PreferredAccountWorker.doInBackground", "missing READ_CONTACTS permission");
249       return Optional.absent();
250     }
251 
252     if (TextUtils.isEmpty(phoneNumber)) {
253       return Optional.absent();
254     }
255     try (Cursor cursor =
256         appContext
257             .getContentResolver()
258             .query(
259                 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)),
260                 new String[] {PhoneLookup.DATA_ID},
261                 null,
262                 null,
263                 null)) {
264       if (cursor == null) {
265         return Optional.absent();
266       }
267       ImmutableSet<String> validAccountTypes =
268           PreferredAccountUtil.getValidAccountTypes(appContext);
269       String result = null;
270       while (cursor.moveToNext()) {
271         Optional<String> accountType =
272             getAccountType(appContext.getContentResolver(), cursor.getLong(0));
273         if (accountType.isPresent() && !validAccountTypes.contains(accountType.get())) {
274           // Empty accountType is treated as writable
275           LogUtil.i("CallingAccountSelector.getDataId", "ignoring non-writable " + accountType);
276           continue;
277         }
278         if (result != null && !result.equals(cursor.getString(0))) {
279           // TODO(twyen): if there are multiple entries attempt to grab from the contact that
280           // initiated the call.
281           LogUtil.i("CallingAccountSelector.getDataId", "lookup result not unique, ignoring");
282           return Optional.absent();
283         }
284         result = cursor.getString(0);
285       }
286       return Optional.fromNullable(result);
287     }
288   }
289 
290   @WorkerThread
getAccountType(ContentResolver contentResolver, long dataId)291   private static Optional<String> getAccountType(ContentResolver contentResolver, long dataId) {
292     Assert.isWorkerThread();
293     Optional<Long> rawContactId = getRawContactId(contentResolver, dataId);
294     if (!rawContactId.isPresent()) {
295       return Optional.absent();
296     }
297     try (Cursor cursor =
298         contentResolver.query(
299             ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId.get()),
300             new String[] {RawContacts.ACCOUNT_TYPE},
301             null,
302             null,
303             null)) {
304       if (cursor == null || !cursor.moveToFirst()) {
305         return Optional.absent();
306       }
307       return Optional.fromNullable(cursor.getString(0));
308     }
309   }
310 
311   @WorkerThread
getRawContactId(ContentResolver contentResolver, long dataId)312   private static Optional<Long> getRawContactId(ContentResolver contentResolver, long dataId) {
313     Assert.isWorkerThread();
314     try (Cursor cursor =
315         contentResolver.query(
316             ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
317             new String[] {Data.RAW_CONTACT_ID},
318             null,
319             null,
320             null)) {
321       if (cursor == null || !cursor.moveToFirst()) {
322         return Optional.absent();
323       }
324       return Optional.of(cursor.getLong(0));
325     }
326   }
327 
328   @WorkerThread
329   @NonNull
getPreferredAccount( @onNull Context context, @NonNull String dataId)330   private static Optional<PhoneAccountHandle> getPreferredAccount(
331       @NonNull Context context, @NonNull String dataId) {
332     Assert.isWorkerThread();
333     Assert.isNotNull(dataId);
334     try (Cursor cursor =
335         context
336             .getContentResolver()
337             .query(
338                 PreferredSimFallbackContract.CONTENT_URI,
339                 new String[] {
340                   PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME,
341                   PreferredSim.PREFERRED_PHONE_ACCOUNT_ID
342                 },
343                 PreferredSim.DATA_ID + " = ?",
344                 new String[] {dataId},
345                 null)) {
346       if (cursor == null) {
347         return Optional.absent();
348       }
349       if (!cursor.moveToFirst()) {
350         return Optional.absent();
351       }
352       return PreferredAccountUtil.getValidPhoneAccount(
353           context, cursor.getString(0), cursor.getString(1));
354     }
355   }
356 
357   @WorkerThread
isPreferredSimEnabled(Context context)358   private static boolean isPreferredSimEnabled(Context context) {
359     Assert.isWorkerThread();
360     if (!ConfigProviderComponent.get(context)
361         .getConfigProvider()
362         .getBoolean("preferred_sim_enabled", true)) {
363       return false;
364     }
365 
366     Intent quickContactIntent = getQuickContactIntent();
367     ResolveInfo resolveInfo =
368         context
369             .getPackageManager()
370             .resolveActivity(quickContactIntent, PackageManager.GET_META_DATA);
371     if (resolveInfo == null
372         || resolveInfo.activityInfo == null
373         || resolveInfo.activityInfo.applicationInfo == null
374         || resolveInfo.activityInfo.applicationInfo.metaData == null) {
375       LogUtil.e("CallingAccountSelector.isPreferredSimEnabled", "cannot resolve quick contact app");
376       return false;
377     }
378     if (!resolveInfo.activityInfo.applicationInfo.metaData.getBoolean(
379         METADATA_SUPPORTS_PREFERRED_SIM, false)) {
380       LogUtil.i(
381           "CallingAccountSelector.isPreferredSimEnabled",
382           "system contacts does not support preferred SIM");
383       return false;
384     }
385     return true;
386   }
387 
388   @VisibleForTesting
getQuickContactIntent()389   public static Intent getQuickContactIntent() {
390     Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT);
391     intent.addCategory(Intent.CATEGORY_DEFAULT);
392     intent.setData(Contacts.CONTENT_URI.buildUpon().appendPath("1").build());
393     return intent;
394   }
395 
396   /**
397    * Most devices are DSDS (dual SIM dual standby) which only one SIM can have active calls at a
398    * time. TODO(twyen): support other dual SIM modes when the API is exposed.
399    */
isSelectable(PhoneAccountHandle phoneAccountHandle)400   private boolean isSelectable(PhoneAccountHandle phoneAccountHandle) {
401     ImmutableList<ActiveCallInfo> activeCalls =
402         ActiveCallsComponent.get(appContext).activeCalls().getActiveCalls();
403     if (activeCalls.isEmpty()) {
404       return true;
405     }
406     for (ActiveCallInfo activeCall : activeCalls) {
407       if (Objects.equals(phoneAccountHandle, activeCall.phoneAccountHandle().orNull())) {
408         return true;
409       }
410     }
411     return false;
412   }
413 
getActiveCallLabel()414   private Optional<String> getActiveCallLabel() {
415     ImmutableList<ActiveCallInfo> activeCalls =
416         ActiveCallsComponent.get(appContext).activeCalls().getActiveCalls();
417 
418     if (activeCalls.isEmpty()) {
419       LogUtil.e("CallingAccountSelector.getActiveCallLabel", "active calls no longer exist");
420       return Optional.absent();
421     }
422     ActiveCallInfo activeCall = activeCalls.get(0);
423     if (!activeCall.phoneAccountHandle().isPresent()) {
424       LogUtil.e("CallingAccountSelector.getActiveCallLabel", "active call has no phone account");
425       return Optional.absent();
426     }
427     PhoneAccount phoneAccount =
428         appContext
429             .getSystemService(TelecomManager.class)
430             .getPhoneAccount(activeCall.phoneAccountHandle().get());
431     if (phoneAccount == null) {
432       LogUtil.e("CallingAccountSelector.getActiveCallLabel", "phone account not found");
433       return Optional.absent();
434     }
435     return Optional.of(phoneAccount.getLabel().toString());
436   }
437 }
438