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