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