1 /*
2  * Copyright (C) 2012 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.bluetooth;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.AppOpsManager;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothDevice;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.ContextWrapper;
27 import android.content.pm.PackageManager;
28 import android.content.pm.UserInfo;
29 import android.location.LocationManager;
30 import android.net.Uri;
31 import android.os.Binder;
32 import android.os.Build;
33 import android.os.ParcelUuid;
34 import android.os.Process;
35 import android.os.SystemProperties;
36 import android.os.UserHandle;
37 import android.os.UserManager;
38 import android.provider.Telephony;
39 import android.util.Log;
40 
41 import org.xmlpull.v1.XmlPullParser;
42 import org.xmlpull.v1.XmlPullParserException;
43 
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.OutputStream;
47 import java.nio.ByteBuffer;
48 import java.nio.ByteOrder;
49 import java.nio.charset.Charset;
50 import java.nio.charset.CharsetDecoder;
51 import java.time.Instant;
52 import java.time.ZoneId;
53 import java.time.format.DateTimeFormatter;
54 import java.util.UUID;
55 import java.util.concurrent.TimeUnit;
56 
57 /**
58  * @hide
59  */
60 
61 public final class Utils {
62     private static final String TAG = "BluetoothUtils";
63     private static final int MICROS_PER_UNIT = 625;
64     private static final String PTS_TEST_MODE_PROPERTY = "persist.bluetooth.pts";
65 
66     static final int BD_ADDR_LEN = 6; // bytes
67     static final int BD_UUID_LEN = 16; // bytes
68 
69     /*
70      * Special characters
71      *
72      * (See "What is a phone number?" doc)
73      * 'p' --- GSM pause character, same as comma
74      * 'n' --- GSM wild character
75      * 'w' --- GSM wait character
76      */
77     public static final char PAUSE = ',';
78     public static final char WAIT = ';';
79 
isPause(char c)80     private static boolean isPause(char c) {
81         return c == 'p' || c == 'P';
82     }
83 
isToneWait(char c)84     private static boolean isToneWait(char c) {
85         return c == 'w' || c == 'W';
86     }
87 
getAddressStringFromByte(byte[] address)88     public static String getAddressStringFromByte(byte[] address) {
89         if (address == null || address.length != BD_ADDR_LEN) {
90             return null;
91         }
92 
93         return String.format("%02X:%02X:%02X:%02X:%02X:%02X", address[0], address[1], address[2],
94                 address[3], address[4], address[5]);
95     }
96 
getByteAddress(BluetoothDevice device)97     public static byte[] getByteAddress(BluetoothDevice device) {
98         return getBytesFromAddress(device.getAddress());
99     }
100 
addressToBytes(String address)101     public static byte[] addressToBytes(String address) {
102         return getBytesFromAddress(address);
103     }
104 
getBytesFromAddress(String address)105     public static byte[] getBytesFromAddress(String address) {
106         int i, j = 0;
107         byte[] output = new byte[BD_ADDR_LEN];
108 
109         for (i = 0; i < address.length(); i++) {
110             if (address.charAt(i) != ':') {
111                 output[j] = (byte) Integer.parseInt(address.substring(i, i + 2), BD_UUID_LEN);
112                 j++;
113                 i++;
114             }
115         }
116 
117         return output;
118     }
119 
byteArrayToInt(byte[] valueBuf)120     public static int byteArrayToInt(byte[] valueBuf) {
121         return byteArrayToInt(valueBuf, 0);
122     }
123 
byteArrayToShort(byte[] valueBuf)124     public static short byteArrayToShort(byte[] valueBuf) {
125         ByteBuffer converter = ByteBuffer.wrap(valueBuf);
126         converter.order(ByteOrder.nativeOrder());
127         return converter.getShort();
128     }
129 
byteArrayToInt(byte[] valueBuf, int offset)130     public static int byteArrayToInt(byte[] valueBuf, int offset) {
131         ByteBuffer converter = ByteBuffer.wrap(valueBuf);
132         converter.order(ByteOrder.nativeOrder());
133         return converter.getInt(offset);
134     }
135 
byteArrayToString(byte[] valueBuf)136     public static String byteArrayToString(byte[] valueBuf) {
137         StringBuilder sb = new StringBuilder();
138         for (int idx = 0; idx < valueBuf.length; idx++) {
139             if (idx != 0) {
140                 sb.append(" ");
141             }
142             sb.append(String.format("%02x", valueBuf[idx]));
143         }
144         return sb.toString();
145     }
146 
147     /**
148      * A parser to transfer a byte array to a UTF8 string
149      *
150      * @param valueBuf the byte array to transfer
151      * @return the transferred UTF8 string
152      */
byteArrayToUtf8String(byte[] valueBuf)153     public static String byteArrayToUtf8String(byte[] valueBuf) {
154         CharsetDecoder decoder = Charset.forName("UTF8").newDecoder();
155         ByteBuffer byteBuffer = ByteBuffer.wrap(valueBuf);
156         String valueStr = "";
157         try {
158             valueStr = decoder.decode(byteBuffer).toString();
159         } catch (Exception ex) {
160             Log.e(TAG, "Error when parsing byte array to UTF8 String. " + ex);
161         }
162         return valueStr;
163     }
164 
intToByteArray(int value)165     public static byte[] intToByteArray(int value) {
166         ByteBuffer converter = ByteBuffer.allocate(4);
167         converter.order(ByteOrder.nativeOrder());
168         converter.putInt(value);
169         return converter.array();
170     }
171 
uuidToByteArray(ParcelUuid pUuid)172     public static byte[] uuidToByteArray(ParcelUuid pUuid) {
173         int length = BD_UUID_LEN;
174         ByteBuffer converter = ByteBuffer.allocate(length);
175         converter.order(ByteOrder.BIG_ENDIAN);
176         long msb, lsb;
177         UUID uuid = pUuid.getUuid();
178         msb = uuid.getMostSignificantBits();
179         lsb = uuid.getLeastSignificantBits();
180         converter.putLong(msb);
181         converter.putLong(8, lsb);
182         return converter.array();
183     }
184 
uuidsToByteArray(ParcelUuid[] uuids)185     public static byte[] uuidsToByteArray(ParcelUuid[] uuids) {
186         int length = uuids.length * BD_UUID_LEN;
187         ByteBuffer converter = ByteBuffer.allocate(length);
188         converter.order(ByteOrder.BIG_ENDIAN);
189         UUID uuid;
190         long msb, lsb;
191         for (int i = 0; i < uuids.length; i++) {
192             uuid = uuids[i].getUuid();
193             msb = uuid.getMostSignificantBits();
194             lsb = uuid.getLeastSignificantBits();
195             converter.putLong(i * BD_UUID_LEN, msb);
196             converter.putLong(i * BD_UUID_LEN + 8, lsb);
197         }
198         return converter.array();
199     }
200 
byteArrayToUuid(byte[] val)201     public static ParcelUuid[] byteArrayToUuid(byte[] val) {
202         int numUuids = val.length / BD_UUID_LEN;
203         ParcelUuid[] puuids = new ParcelUuid[numUuids];
204         UUID uuid;
205         int offset = 0;
206 
207         ByteBuffer converter = ByteBuffer.wrap(val);
208         converter.order(ByteOrder.BIG_ENDIAN);
209 
210         for (int i = 0; i < numUuids; i++) {
211             puuids[i] = new ParcelUuid(
212                     new UUID(converter.getLong(offset), converter.getLong(offset + 8)));
213             offset += BD_UUID_LEN;
214         }
215         return puuids;
216     }
217 
debugGetAdapterStateString(int state)218     public static String debugGetAdapterStateString(int state) {
219         switch (state) {
220             case BluetoothAdapter.STATE_OFF:
221                 return "STATE_OFF";
222             case BluetoothAdapter.STATE_ON:
223                 return "STATE_ON";
224             case BluetoothAdapter.STATE_TURNING_ON:
225                 return "STATE_TURNING_ON";
226             case BluetoothAdapter.STATE_TURNING_OFF:
227                 return "STATE_TURNING_OFF";
228             default:
229                 return "UNKNOWN";
230         }
231     }
232 
ellipsize(String s)233     public static String ellipsize(String s) {
234         // Only ellipsize release builds
235         if (!Build.TYPE.equals("user")) {
236             return s;
237         }
238         if (s == null) {
239             return null;
240         }
241         if (s.length() < 3) {
242             return s;
243         }
244         return s.charAt(0) + "⋯" + s.charAt(s.length() - 1);
245     }
246 
copyStream(InputStream is, OutputStream os, int bufferSize)247     public static void copyStream(InputStream is, OutputStream os, int bufferSize)
248             throws IOException {
249         if (is != null && os != null) {
250             byte[] buffer = new byte[bufferSize];
251             int bytesRead = 0;
252             while ((bytesRead = is.read(buffer)) >= 0) {
253                 os.write(buffer, 0, bytesRead);
254             }
255         }
256     }
257 
safeCloseStream(InputStream is)258     public static void safeCloseStream(InputStream is) {
259         if (is != null) {
260             try {
261                 is.close();
262             } catch (Throwable t) {
263                 Log.d(TAG, "Error closing stream", t);
264             }
265         }
266     }
267 
safeCloseStream(OutputStream os)268     public static void safeCloseStream(OutputStream os) {
269         if (os != null) {
270             try {
271                 os.close();
272             } catch (Throwable t) {
273                 Log.d(TAG, "Error closing stream", t);
274             }
275         }
276     }
277 
278     static int sSystemUiUid = UserHandle.USER_NULL;
setSystemUiUid(int uid)279     public static void setSystemUiUid(int uid) {
280         Utils.sSystemUiUid = uid;
281     }
282 
283     static int sForegroundUserId = UserHandle.USER_NULL;
setForegroundUserId(int uid)284     public static void setForegroundUserId(int uid) {
285         Utils.sForegroundUserId = uid;
286     }
287 
enforceBluetoothPermission(Context context)288     public static void enforceBluetoothPermission(Context context) {
289         context.enforceCallingOrSelfPermission(
290                 android.Manifest.permission.BLUETOOTH,
291                 "Need BLUETOOTH permission");
292     }
293 
enforceBluetoothAdminPermission(Context context)294     public static void enforceBluetoothAdminPermission(Context context) {
295         context.enforceCallingOrSelfPermission(
296                 android.Manifest.permission.BLUETOOTH_ADMIN,
297                 "Need BLUETOOTH ADMIN permission");
298     }
299 
enforceBluetoothPrivilegedPermission(Context context)300     public static void enforceBluetoothPrivilegedPermission(Context context) {
301         context.enforceCallingOrSelfPermission(
302                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
303                 "Need BLUETOOTH PRIVILEGED permission");
304     }
305 
enforceLocalMacAddressPermission(Context context)306     public static void enforceLocalMacAddressPermission(Context context) {
307         context.enforceCallingOrSelfPermission(
308                 android.Manifest.permission.LOCAL_MAC_ADDRESS,
309                 "Need LOCAL_MAC_ADDRESS permission");
310     }
311 
enforceDumpPermission(Context context)312     public static void enforceDumpPermission(Context context) {
313         context.enforceCallingOrSelfPermission(
314                 android.Manifest.permission.DUMP,
315                 "Need DUMP permission");
316     }
317 
callerIsSystemOrActiveUser(String tag, String method)318     public static boolean callerIsSystemOrActiveUser(String tag, String method) {
319         if (!checkCaller()) {
320           Log.w(TAG, method + "() - Not allowed for non-active user and non-system user");
321           return false;
322         }
323         return true;
324     }
325 
callerIsSystemOrActiveOrManagedUser(Context context, String tag, String method)326     public static boolean callerIsSystemOrActiveOrManagedUser(Context context, String tag, String method) {
327         if (!checkCallerAllowManagedProfiles(context)) {
328           Log.w(TAG, method + "() - Not allowed for non-active user and non-system and non-managed user");
329           return false;
330         }
331         return true;
332     }
333 
checkCaller()334     public static boolean checkCaller() {
335         int callingUser = UserHandle.getCallingUserId();
336         int callingUid = Binder.getCallingUid();
337         return (sForegroundUserId == callingUser)
338                 || (UserHandle.getAppId(sSystemUiUid) == UserHandle.getAppId(callingUid))
339                 || (UserHandle.getAppId(Process.SYSTEM_UID) == UserHandle.getAppId(callingUid));
340     }
341 
checkCallerAllowManagedProfiles(Context mContext)342     public static boolean checkCallerAllowManagedProfiles(Context mContext) {
343         if (mContext == null) {
344             return checkCaller();
345         }
346         int callingUser = UserHandle.getCallingUserId();
347         int callingUid = Binder.getCallingUid();
348 
349         // Use the Bluetooth process identity when making call to get parent user
350         long ident = Binder.clearCallingIdentity();
351         try {
352             UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
353             UserInfo ui = um.getProfileParent(callingUser);
354             int parentUser = (ui != null) ? ui.id : UserHandle.USER_NULL;
355 
356             // Always allow SystemUI/System access.
357             return (sForegroundUserId == callingUser) || (sForegroundUserId == parentUser)
358                     || (UserHandle.getAppId(sSystemUiUid) == UserHandle.getAppId(callingUid))
359                     || (UserHandle.getAppId(Process.SYSTEM_UID) == UserHandle.getAppId(callingUid));
360         } catch (Exception ex) {
361             Log.e(TAG, "checkCallerAllowManagedProfiles: Exception ex=" + ex);
362             return false;
363         } finally {
364             Binder.restoreCallingIdentity(ident);
365         }
366     }
367 
368     /**
369      * Enforce the context has android.Manifest.permission.BLUETOOTH_ADMIN permission. A
370      * {@link SecurityException} would be thrown if neither the calling process or the application
371      * does not have BLUETOOTH_ADMIN permission.
372      *
373      * @param context Context for the permission check.
374      */
enforceAdminPermission(ContextWrapper context)375     public static void enforceAdminPermission(ContextWrapper context) {
376         context.enforceCallingOrSelfPermission(android.Manifest.permission.BLUETOOTH_ADMIN,
377                 "Need BLUETOOTH_ADMIN permission");
378     }
379 
380     /**
381      * Checks whether location is off and must be on for us to perform some operation
382      */
blockedByLocationOff(Context context, UserHandle userHandle)383     public static boolean blockedByLocationOff(Context context, UserHandle userHandle) {
384         return !context.getSystemService(LocationManager.class)
385                 .isLocationEnabledForUser(userHandle);
386     }
387 
388     /**
389      * Checks that calling process has android.Manifest.permission.ACCESS_COARSE_LOCATION and
390      * OP_COARSE_LOCATION is allowed
391      */
checkCallerHasCoarseLocation(Context context, AppOpsManager appOps, String callingPackage, @Nullable String callingFeatureId, UserHandle userHandle)392     public static boolean checkCallerHasCoarseLocation(Context context, AppOpsManager appOps,
393             String callingPackage, @Nullable String callingFeatureId, UserHandle userHandle) {
394         if (blockedByLocationOff(context, userHandle)) {
395             Log.e(TAG, "Permission denial: Location is off.");
396             return false;
397         }
398 
399         // Check coarse, but note fine
400         if (context.checkCallingOrSelfPermission(
401                 android.Manifest.permission.ACCESS_COARSE_LOCATION)
402                         == PackageManager.PERMISSION_GRANTED
403                 && isAppOppAllowed(appOps, AppOpsManager.OPSTR_FINE_LOCATION, callingPackage,
404                 callingFeatureId)) {
405             return true;
406         }
407 
408         Log.e(TAG, "Permission denial: Need ACCESS_COARSE_LOCATION "
409                 + "permission to get scan results");
410         return false;
411     }
412 
413     /**
414      * Checks that calling process has android.Manifest.permission.ACCESS_COARSE_LOCATION and
415      * OP_COARSE_LOCATION is allowed or android.Manifest.permission.ACCESS_FINE_LOCATION and
416      * OP_FINE_LOCATION is allowed
417      */
checkCallerHasCoarseOrFineLocation(Context context, AppOpsManager appOps, String callingPackage, @Nullable String callingFeatureId, UserHandle userHandle)418     public static boolean checkCallerHasCoarseOrFineLocation(Context context, AppOpsManager appOps,
419             String callingPackage, @Nullable String callingFeatureId, UserHandle userHandle) {
420         if (blockedByLocationOff(context, userHandle)) {
421             Log.e(TAG, "Permission denial: Location is off.");
422             return false;
423         }
424 
425         if (context.checkCallingOrSelfPermission(
426                 android.Manifest.permission.ACCESS_FINE_LOCATION)
427                         == PackageManager.PERMISSION_GRANTED
428                 && isAppOppAllowed(appOps, AppOpsManager.OPSTR_FINE_LOCATION, callingPackage,
429                 callingFeatureId)) {
430             return true;
431         }
432 
433         // Check coarse, but note fine
434         if (context.checkCallingOrSelfPermission(
435                 android.Manifest.permission.ACCESS_COARSE_LOCATION)
436                         == PackageManager.PERMISSION_GRANTED
437                 && isAppOppAllowed(appOps, AppOpsManager.OPSTR_FINE_LOCATION, callingPackage,
438                 callingFeatureId)) {
439             return true;
440         }
441 
442         Log.e(TAG, "Permission denial: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION"
443                 + "permission to get scan results");
444         return false;
445     }
446 
447     /**
448      * Checks that calling process has android.Manifest.permission.ACCESS_FINE_LOCATION and
449      * OP_FINE_LOCATION is allowed
450      */
checkCallerHasFineLocation(Context context, AppOpsManager appOps, String callingPackage, @Nullable String callingFeatureId, UserHandle userHandle)451     public static boolean checkCallerHasFineLocation(Context context, AppOpsManager appOps,
452             String callingPackage, @Nullable String callingFeatureId, UserHandle userHandle) {
453         if (blockedByLocationOff(context, userHandle)) {
454             Log.e(TAG, "Permission denial: Location is off.");
455             return false;
456         }
457 
458         if (context.checkCallingOrSelfPermission(
459                 android.Manifest.permission.ACCESS_FINE_LOCATION)
460                         == PackageManager.PERMISSION_GRANTED
461                 && isAppOppAllowed(appOps, AppOpsManager.OPSTR_FINE_LOCATION, callingPackage,
462                 callingFeatureId)) {
463             return true;
464         }
465 
466         Log.e(TAG, "Permission denial: Need ACCESS_FINE_LOCATION "
467                 + "permission to get scan results");
468         return false;
469     }
470 
471     /**
472      * Returns true if the caller holds NETWORK_SETTINGS
473      */
checkCallerHasNetworkSettingsPermission(Context context)474     public static boolean checkCallerHasNetworkSettingsPermission(Context context) {
475         return context.checkCallingOrSelfPermission(android.Manifest.permission.NETWORK_SETTINGS)
476                 == PackageManager.PERMISSION_GRANTED;
477     }
478 
479     /**
480      * Returns true if the caller holds NETWORK_SETUP_WIZARD
481      */
checkCallerHasNetworkSetupWizardPermission(Context context)482     public static boolean checkCallerHasNetworkSetupWizardPermission(Context context) {
483         return context.checkCallingOrSelfPermission(
484                 android.Manifest.permission.NETWORK_SETUP_WIZARD)
485                         == PackageManager.PERMISSION_GRANTED;
486     }
487 
isQApp(Context context, String pkgName)488     public static boolean isQApp(Context context, String pkgName) {
489         try {
490             return context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion
491                     >= Build.VERSION_CODES.Q;
492         } catch (PackageManager.NameNotFoundException e) {
493             // In case of exception, assume Q app
494         }
495         return true;
496     }
497 
isAppOppAllowed(AppOpsManager appOps, String op, String callingPackage, @NonNull String callingFeatureId)498     private static boolean isAppOppAllowed(AppOpsManager appOps, String op, String callingPackage,
499             @NonNull String callingFeatureId) {
500         return appOps.noteOp(op, Binder.getCallingUid(), callingPackage)
501                 == AppOpsManager.MODE_ALLOWED;
502     }
503 
504     /**
505      * Converts {@code millisecond} to unit. Each unit is 0.625 millisecond.
506      */
millsToUnit(int milliseconds)507     public static int millsToUnit(int milliseconds) {
508         return (int) (TimeUnit.MILLISECONDS.toMicros(milliseconds) / MICROS_PER_UNIT);
509     }
510 
511     /**
512      * Check if we are running in BluetoothInstrumentationTest context by trying to load
513      * com.android.bluetooth.FileSystemWriteTest. If we are not in Instrumentation test mode, this
514      * class should not be found. Thus, the assumption is that FileSystemWriteTest must exist.
515      * If FileSystemWriteTest is removed in the future, another test class in
516      * BluetoothInstrumentationTest should be used instead
517      *
518      * @return true if in BluetoothInstrumentationTest, false otherwise
519      */
isInstrumentationTestMode()520     public static boolean isInstrumentationTestMode() {
521         try {
522             return Class.forName("com.android.bluetooth.FileSystemWriteTest") != null;
523         } catch (ClassNotFoundException exception) {
524             return false;
525         }
526     }
527 
528     /**
529      * Throws {@link IllegalStateException} if we are not in BluetoothInstrumentationTest. Useful
530      * for ensuring certain methods only get called in BluetoothInstrumentationTest
531      */
enforceInstrumentationTestMode()532     public static void enforceInstrumentationTestMode() {
533         if (!isInstrumentationTestMode()) {
534             throw new IllegalStateException("Not in BluetoothInstrumentationTest");
535         }
536     }
537 
538     /**
539      * Check if we are running in PTS test mode. To enable/disable PTS test mode, invoke
540      * {@code adb shell setprop persist.bluetooth.pts true/false}
541      *
542      * @return true if in PTS Test mode, false otherwise
543      */
isPtsTestMode()544     public static boolean isPtsTestMode() {
545         return SystemProperties.getBoolean(PTS_TEST_MODE_PROPERTY, false);
546     }
547 
548     /**
549      * Get uid/pid string in a binder call
550      *
551      * @return "uid/pid=xxxx/yyyy"
552      */
getUidPidString()553     public static String getUidPidString() {
554         return "uid/pid=" + Binder.getCallingUid() + "/" + Binder.getCallingPid();
555     }
556 
557     /**
558      * Get system local time
559      *
560      * @return "MM-dd HH:mm:ss.SSS"
561      */
getLocalTimeString()562     public static String getLocalTimeString() {
563         return DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS")
564                 .withZone(ZoneId.systemDefault()).format(Instant.now());
565     }
566 
skipCurrentTag(XmlPullParser parser)567     public static void skipCurrentTag(XmlPullParser parser)
568             throws XmlPullParserException, IOException {
569         int outerDepth = parser.getDepth();
570         int type;
571         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
572                 && (type != XmlPullParser.END_TAG
573                 || parser.getDepth() > outerDepth)) {
574         }
575     }
576 
577     /**
578      * Converts pause and tonewait pause characters
579      * to Android representation.
580      * RFC 3601 says pause is 'p' and tonewait is 'w'.
581      */
convertPreDial(String phoneNumber)582     public static String convertPreDial(String phoneNumber) {
583         if (phoneNumber == null) {
584             return null;
585         }
586         int len = phoneNumber.length();
587         StringBuilder ret = new StringBuilder(len);
588 
589         for (int i = 0; i < len; i++) {
590             char c = phoneNumber.charAt(i);
591 
592             if (isPause(c)) {
593                 c = PAUSE;
594             } else if (isToneWait(c)) {
595                 c = WAIT;
596             }
597             ret.append(c);
598         }
599         return ret.toString();
600     }
601 
602     /**
603      * Move a message to the given folder.
604      *
605      * @param context the context to use
606      * @param uri the message to move
607      * @param messageSent if the message is SENT or FAILED
608      * @return true if the operation succeeded
609      */
moveMessageToFolder(Context context, Uri uri, boolean messageSent)610     public static boolean moveMessageToFolder(Context context, Uri uri, boolean messageSent) {
611         if (uri == null) {
612             return false;
613         }
614 
615         ContentValues values = new ContentValues(3);
616         if (messageSent) {
617             values.put(Telephony.Sms.READ, 1);
618             values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_SENT);
619         } else {
620             values.put(Telephony.Sms.READ, 0);
621             values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_FAILED);
622         }
623         values.put(Telephony.Sms.ERROR_CODE, 0);
624 
625         return 1 == context.getContentResolver().update(uri, values, null, null);
626     }
627 }
628