• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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