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