1 /* 2 * Copyright (C) 2011 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.server.telecom; 18 19 // TODO: Needed for move to system service: import com.android.internal.R; 20 import android.app.Activity; 21 import android.app.PendingIntent; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.content.SharedPreferences; 27 import android.content.res.Resources; 28 import android.os.Looper; 29 import android.telecom.Connection; 30 import android.telecom.Log; 31 import android.telecom.Logging.Session; 32 import android.telephony.PhoneNumberUtils; 33 import android.telephony.SmsManager; 34 import android.telephony.SubscriptionManager; 35 import android.text.BidiFormatter; 36 import android.text.Spannable; 37 import android.text.SpannableString; 38 import android.text.TextUtils; 39 import android.widget.Toast; 40 41 import com.android.server.telecom.flags.FeatureFlags; 42 43 import java.text.Bidi; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.concurrent.CompletableFuture; 47 import java.util.concurrent.Executor; 48 49 /** 50 * Helper class to manage the "Respond via Message" feature for incoming calls. 51 */ 52 public class RespondViaSmsManager extends CallsManagerListenerBase { 53 private static final String ACTION_MESSAGE_SENT = "com.android.server.telecom.MESSAGE_SENT"; 54 55 private static final class MessageSentReceiver extends BroadcastReceiver { 56 private final String mContactName; 57 private final int mNumMessageParts; 58 private int mNumMessagesSent = 0; MessageSentReceiver(String contactName, int numMessageParts)59 MessageSentReceiver(String contactName, int numMessageParts) { 60 mContactName = contactName; 61 mNumMessageParts = numMessageParts; 62 } 63 64 @Override onReceive(Context context, Intent intent)65 public void onReceive(Context context, Intent intent) { 66 if (getResultCode() == Activity.RESULT_OK) { 67 mNumMessagesSent++; 68 if (mNumMessagesSent == mNumMessageParts) { 69 showMessageResultToast(mContactName, context, true); 70 context.unregisterReceiver(this); 71 } 72 } else { 73 context.unregisterReceiver(this); 74 showMessageResultToast(mContactName, context, false); 75 Log.w(RespondViaSmsManager.class.getSimpleName(), 76 "Message failed with error %s", getResultCode()); 77 } 78 } 79 } 80 81 private final CallsManager mCallsManager; 82 private final TelecomSystem.SyncRoot mLock; 83 private final Executor mAsyncExecutor; 84 private final FeatureFlags mFeatureFlags; 85 RespondViaSmsManager(CallsManager callsManager, TelecomSystem.SyncRoot lock, Executor asyncExecutor, FeatureFlags featureFlags)86 public RespondViaSmsManager(CallsManager callsManager, TelecomSystem.SyncRoot lock, 87 Executor asyncExecutor, FeatureFlags featureFlags) { 88 mCallsManager = callsManager; 89 mLock = lock; 90 mAsyncExecutor = asyncExecutor; 91 mFeatureFlags = featureFlags; 92 } 93 94 /** 95 * Read the (customizable) canned responses from SharedPreferences, 96 * or from defaults if the user has never actually brought up 97 * the Settings UI. 98 * 99 * The interface of this method is asynchronous since it does disk I/O. 100 * 101 * @param response An object to receive an async reply, which will be called from 102 * the main thread. 103 * @param context The context. 104 */ loadCannedTextMessages(final CallsManager.Response<Void, List<String>> response, final Context context)105 public void loadCannedTextMessages(final CallsManager.Response<Void, List<String>> response, 106 final Context context) { 107 if (mFeatureFlags.enableRespondViaSmsManagerAsync()) { 108 CompletableFuture<List<String>> cannedTextMessages = new CompletableFuture<>(); 109 Session s = Log.createSubsession(); 110 mAsyncExecutor.execute(() -> { 111 try { 112 Log.continueSession(s, "RVSM.lCTM.e"); 113 cannedTextMessages.complete(loadCannedTextMessages(context)); 114 } finally { 115 Log.endSession(); 116 } 117 }); 118 cannedTextMessages.whenCompleteAsync((result, exception) -> { 119 if (exception != null) { 120 Log.e(RespondViaSmsManager.class.getSimpleName(), exception, 121 "loadCannedTextMessages failed"); 122 response.onError(null, -1, exception.toString()); 123 } else { 124 response.onResult(null, result); 125 } 126 }, new LoggedHandlerExecutor(context.getMainThreadHandler(), "RVSM.lCTM.c", mLock)); 127 128 } else { 129 new Thread() { 130 @Override 131 public void run() { 132 List<String> textMessages = loadCannedTextMessages(context); 133 synchronized (mLock) { 134 response.onResult(null, textMessages); 135 } 136 } 137 }.start(); 138 } 139 } 140 loadCannedTextMessages(final Context context)141 private List<String> loadCannedTextMessages(final Context context) { 142 Log.d(RespondViaSmsManager.this, "loadCannedTextMessages() starting"); 143 // This function guarantees that QuickResponses will be in our 144 // SharedPreferences with the proper values considering there may be 145 // old QuickResponses in Telephony pre L. 146 QuickResponseUtils.maybeMigrateLegacyQuickResponses(context); 147 148 final SharedPreferences prefs = context.getSharedPreferences( 149 QuickResponseUtils.SHARED_PREFERENCES_NAME, 150 Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS); 151 final Resources res = context.getResources(); 152 153 final ArrayList<String> textMessages = new ArrayList<>( 154 QuickResponseUtils.NUM_CANNED_RESPONSES); 155 156 // Where the user has changed a quick response back to the same text as the 157 // original text, clear the shared pref. This ensures we always load the resource 158 // in the current active language. 159 QuickResponseUtils.maybeResetQuickResponses(context, prefs); 160 161 // Note the default values here must agree with the corresponding 162 // android:defaultValue attributes in respond_via_sms_settings.xml. 163 textMessages.add(0, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_1, 164 res.getString(R.string.respond_via_sms_canned_response_1))); 165 textMessages.add(1, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_2, 166 res.getString(R.string.respond_via_sms_canned_response_2))); 167 textMessages.add(2, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_3, 168 res.getString(R.string.respond_via_sms_canned_response_3))); 169 textMessages.add(3, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_4, 170 res.getString(R.string.respond_via_sms_canned_response_4))); 171 172 Log.d(RespondViaSmsManager.this, 173 "loadCannedResponses() completed, found responses: %s", 174 textMessages.toString()); 175 return textMessages; 176 } 177 178 @Override onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage)179 public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) { 180 if (rejectWithMessage 181 && call.getHandle() != null 182 && !call.can(Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION)) { 183 int subId = mCallsManager.getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount( 184 call.getTargetPhoneAccount()); 185 rejectCallWithMessage(call.getContext(), call.getHandle().getSchemeSpecificPart(), 186 textMessage, subId, call.getName()); 187 } 188 } 189 showMessageResultToast(final String phoneNumber, final Context context, boolean success)190 private static void showMessageResultToast(final String phoneNumber, 191 final Context context, boolean success) { 192 // ...and show a brief confirmation to the user (since 193 // otherwise it's hard to be sure that anything actually 194 // happened.) 195 final Resources res = context.getResources(); 196 final String formatString = res.getString(success 197 ? R.string.respond_via_sms_confirmation_format 198 : R.string.respond_via_sms_failure_format); 199 final BidiFormatter phoneNumberFormatter = BidiFormatter.getInstance(); 200 final String confirmationMsg = String.format(formatString, 201 phoneNumberFormatter.unicodeWrap(phoneNumber)); 202 int startingPosition = confirmationMsg.indexOf(phoneNumber); 203 int endingPosition = startingPosition + phoneNumber.length(); 204 205 Spannable styledConfirmationMsg = new SpannableString(confirmationMsg); 206 PhoneNumberUtils.addTtsSpan(styledConfirmationMsg, startingPosition, endingPosition); 207 Toast.makeText(context, styledConfirmationMsg, 208 Toast.LENGTH_LONG).show(); 209 210 // TODO: If the device is locked, this toast won't actually ever 211 // be visible! (That's because we're about to dismiss the call 212 // screen, which means that the device will return to the 213 // keyguard. But toasts aren't visible on top of the keyguard.) 214 // Possible fixes: 215 // (1) Is it possible to allow a specific Toast to be visible 216 // on top of the keyguard? 217 // (2) Artificially delay the dismissCallScreen() call by 3 218 // seconds to allow the toast to be seen? 219 // (3) Don't use a toast at all; instead use a transient state 220 // of the InCallScreen (perhaps via the InCallUiState 221 // progressIndication feature), and have that state be 222 // visible for 3 seconds before calling dismissCallScreen(). 223 } 224 225 /** 226 * Reject the call with the specified message. If message is null this call is ignored. 227 */ rejectCallWithMessage(Context context, String phoneNumber, String textMessage, int subId, String contactName)228 private void rejectCallWithMessage(Context context, String phoneNumber, String textMessage, 229 int subId, String contactName) { 230 if (TextUtils.isEmpty(textMessage)) { 231 Log.w(RespondViaSmsManager.this, "Couldn't send SMS message: empty text message. "); 232 return; 233 } 234 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 235 Log.w(RespondViaSmsManager.this, "Couldn't send SMS message: Invalid SubId: " + 236 subId); 237 return; 238 } 239 if(mFeatureFlags.enableRespondViaSmsManagerAsync()) { 240 Session s = Log.createSubsession(); 241 mAsyncExecutor.execute(() -> { 242 try { 243 Log.continueSession(s, "RVSM.rCWM.e"); 244 sendTextMessage(context, phoneNumber, textMessage, subId, contactName); 245 } finally { 246 Log.endSession(); 247 } 248 }); 249 } else { 250 sendTextMessage(context, phoneNumber, textMessage, subId, contactName); 251 } 252 } 253 sendTextMessage(Context context, String phoneNumber, String textMessage, int subId, String contactName)254 private void sendTextMessage(Context context, String phoneNumber, String textMessage, 255 int subId, String contactName) { 256 SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId); 257 try { 258 ArrayList<String> messageParts = smsManager.divideMessage(textMessage); 259 ArrayList<PendingIntent> sentIntents = new ArrayList<>(messageParts.size()); 260 for (int i = 0; i < messageParts.size(); i++) { 261 Intent intent = new Intent(ACTION_MESSAGE_SENT); 262 PendingIntent pendingIntent = PendingIntent.getBroadcast(context, i, intent, 263 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 264 sentIntents.add(pendingIntent); 265 } 266 267 MessageSentReceiver receiver = new MessageSentReceiver( 268 !TextUtils.isEmpty(contactName) ? contactName : phoneNumber, 269 messageParts.size()); 270 IntentFilter messageSentFilter = new IntentFilter(ACTION_MESSAGE_SENT); 271 messageSentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 272 context.registerReceiver(receiver, messageSentFilter, Context.RECEIVER_NOT_EXPORTED); 273 smsManager.sendMultipartTextMessage(phoneNumber, null, messageParts, 274 sentIntents/*sentIntent*/, null /*deliveryIntent*/, context.getOpPackageName(), 275 context.getAttributionTag()); 276 } catch (IllegalArgumentException e) { 277 Log.w(RespondViaSmsManager.this, "Couldn't send SMS message: " + 278 e.getMessage()); 279 } 280 } 281 } 282