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