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