1 /* 2 * Copyright (C) 2015 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 package com.android.voicemail.impl.fetch; 17 18 import android.annotation.TargetApi; 19 import android.content.BroadcastReceiver; 20 import android.content.ComponentName; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.database.Cursor; 25 import android.net.Network; 26 import android.net.Uri; 27 import android.os.Build.VERSION_CODES; 28 import android.provider.VoicemailContract; 29 import android.provider.VoicemailContract.Voicemails; 30 import android.support.annotation.NonNull; 31 import android.support.annotation.Nullable; 32 import android.support.v4.os.BuildCompat; 33 import android.telecom.PhoneAccountHandle; 34 import android.telecom.TelecomManager; 35 import android.telephony.TelephonyManager; 36 import android.text.TextUtils; 37 import com.android.voicemail.VoicemailComponent; 38 import com.android.voicemail.impl.VoicemailStatus; 39 import com.android.voicemail.impl.VvmLog; 40 import com.android.voicemail.impl.imap.ImapHelper; 41 import com.android.voicemail.impl.imap.ImapHelper.InitializingException; 42 import com.android.voicemail.impl.sync.VvmAccountManager; 43 import com.android.voicemail.impl.sync.VvmNetworkRequestCallback; 44 import java.util.concurrent.Executor; 45 import java.util.concurrent.Executors; 46 47 /** handles {@link VoicemailContract#ACTION_FETCH_VOICEMAIL} */ 48 @TargetApi(VERSION_CODES.O) 49 public class FetchVoicemailReceiver extends BroadcastReceiver { 50 51 private static final String TAG = "FetchVoicemailReceiver"; 52 53 static final String[] PROJECTION = 54 new String[] { 55 Voicemails.SOURCE_DATA, // 0 56 Voicemails.PHONE_ACCOUNT_ID, // 1 57 Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, // 2 58 }; 59 60 public static final int SOURCE_DATA = 0; 61 public static final int PHONE_ACCOUNT_ID = 1; 62 public static final int PHONE_ACCOUNT_COMPONENT_NAME = 2; 63 64 // Number of retries 65 private static final int NETWORK_RETRY_COUNT = 3; 66 67 private ContentResolver contentResolver; 68 private Uri uri; 69 private VvmNetworkRequestCallback networkCallback; 70 private Context context; 71 private String uid; 72 private PhoneAccountHandle phoneAccount; 73 private int retryCount = NETWORK_RETRY_COUNT; 74 75 @Override onReceive(final Context context, Intent intent)76 public void onReceive(final Context context, Intent intent) { 77 if (!VoicemailComponent.get(context).getVoicemailClient().isVoicemailModuleEnabled()) { 78 return; 79 } 80 if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) { 81 VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received"); 82 this.context = context; 83 contentResolver = context.getContentResolver(); 84 uri = intent.getData(); 85 86 if (uri == null) { 87 VvmLog.w(TAG, VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data"); 88 return; 89 } 90 91 if (!context 92 .getPackageName() 93 .equals(uri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) { 94 // Ignore if the fetch request is for a voicemail not from this package. 95 VvmLog.e(TAG, "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName()); 96 return; 97 } 98 99 Cursor cursor = contentResolver.query(uri, PROJECTION, null, null, null); 100 if (cursor == null) { 101 VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null"); 102 return; 103 } 104 try { 105 if (cursor.moveToFirst()) { 106 uid = cursor.getString(SOURCE_DATA); 107 String accountId = cursor.getString(PHONE_ACCOUNT_ID); 108 if (TextUtils.isEmpty(accountId)) { 109 TelephonyManager telephonyManager = 110 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 111 accountId = telephonyManager.getSimSerialNumber(); 112 113 if (TextUtils.isEmpty(accountId)) { 114 VvmLog.e(TAG, "Account null and no default sim found."); 115 return; 116 } 117 } 118 119 phoneAccount = 120 new PhoneAccountHandle( 121 ComponentName.unflattenFromString(cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)), 122 cursor.getString(PHONE_ACCOUNT_ID)); 123 TelephonyManager telephonyManager = 124 context 125 .getSystemService(TelephonyManager.class) 126 .createForPhoneAccountHandle(phoneAccount); 127 if (telephonyManager == null) { 128 // can happen when trying to fetch voicemails from a SIM that is no longer on the 129 // device 130 VvmLog.e(TAG, "account no longer valid, cannot retrieve message"); 131 return; 132 } 133 if (!VvmAccountManager.isAccountActivated(context, phoneAccount)) { 134 phoneAccount = getAccountFromMarshmallowAccount(context, phoneAccount); 135 if (phoneAccount == null) { 136 VvmLog.w(TAG, "Account not registered - cannot retrieve message."); 137 return; 138 } 139 VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle"); 140 } 141 VvmLog.i(TAG, "Requesting network to fetch voicemail"); 142 networkCallback = new fetchVoicemailNetworkRequestCallback(context, phoneAccount); 143 networkCallback.requestNetwork(); 144 } 145 } finally { 146 cursor.close(); 147 } 148 } 149 } 150 151 /** 152 * In ag/930496 the format of PhoneAccountHandle has changed between Marshmallow and Nougat. This 153 * method attempts to search the account from the old database in registered sources using the old 154 * format. There's a chance of M phone account collisions on multi-SIM devices, but visual 155 * voicemail is not supported on M multi-SIM. 156 */ 157 @Nullable getAccountFromMarshmallowAccount( Context context, PhoneAccountHandle oldAccount)158 private static PhoneAccountHandle getAccountFromMarshmallowAccount( 159 Context context, PhoneAccountHandle oldAccount) { 160 if (!BuildCompat.isAtLeastN()) { 161 return null; 162 } 163 for (PhoneAccountHandle handle : 164 context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) { 165 if (getIccSerialNumberFromFullIccSerialNumber(handle.getId()).equals(oldAccount.getId())) { 166 return handle; 167 } 168 } 169 return null; 170 } 171 172 /** 173 * getIccSerialNumber() is used for ID before N, and getFullIccSerialNumber() after. 174 * getIccSerialNumber() stops at the first hex char. 175 */ 176 @NonNull getIccSerialNumberFromFullIccSerialNumber(@onNull String id)177 private static String getIccSerialNumberFromFullIccSerialNumber(@NonNull String id) { 178 for (int i = 0; i < id.length(); i++) { 179 if (!Character.isDigit(id.charAt(i))) { 180 return id.substring(0, i); 181 } 182 } 183 return id; 184 } 185 186 private class fetchVoicemailNetworkRequestCallback extends VvmNetworkRequestCallback { 187 fetchVoicemailNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount)188 public fetchVoicemailNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount) { 189 super(context, phoneAccount, VoicemailStatus.edit(context, phoneAccount)); 190 } 191 192 @Override onAvailable(final Network network)193 public void onAvailable(final Network network) { 194 super.onAvailable(network); 195 fetchVoicemail(network, getVoicemailStatusEditor()); 196 } 197 } 198 fetchVoicemail(final Network network, final VoicemailStatus.Editor status)199 private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) { 200 Executor executor = Executors.newCachedThreadPool(); 201 executor.execute( 202 new Runnable() { 203 @Override 204 public void run() { 205 if (networkCallback != null) { 206 networkCallback.waitForIpv4(); 207 } 208 try { 209 while (retryCount > 0) { 210 VvmLog.i(TAG, "fetching voicemail, retry count=" + retryCount); 211 try (ImapHelper imapHelper = 212 new ImapHelper(context, phoneAccount, network, status)) { 213 boolean success = 214 imapHelper.fetchVoicemailPayload( 215 new VoicemailFetchedCallback(context, uri, phoneAccount), uid); 216 if (!success && retryCount > 0) { 217 VvmLog.i(TAG, "fetch voicemail failed, retrying"); 218 retryCount--; 219 } else { 220 return; 221 } 222 } catch (InitializingException e) { 223 VvmLog.w(TAG, "Can't retrieve Imap credentials ", e); 224 return; 225 } 226 } 227 } finally { 228 if (networkCallback != null) { 229 networkCallback.releaseNetwork(); 230 } 231 } 232 } 233 }); 234 } 235 } 236