1 /* 2 * Copyright (C) 2020 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 package com.android.systemui.util.settings 17 18 import android.annotation.UserIdInt 19 import android.content.ContentResolver 20 import android.database.ContentObserver 21 import android.net.Uri 22 import android.provider.Settings 23 import android.provider.Settings.SettingNotFoundException 24 import androidx.annotation.AnyThread 25 import androidx.annotation.WorkerThread 26 import com.android.app.tracing.TraceUtils.trace 27 import com.android.app.tracing.coroutines.launchTraced as launch 28 import com.android.app.tracing.coroutines.withContextTraced as withContext 29 import kotlin.coroutines.coroutineContext 30 import kotlinx.coroutines.CoroutineDispatcher 31 import kotlinx.coroutines.CoroutineScope 32 import kotlinx.coroutines.Job 33 34 /** 35 * Used to interact with mainly with Settings.Global, but can also be used for Settings.System and 36 * Settings.Secure. To use the per-user System and Secure settings, [UserSettingsProxy] must be used 37 * instead. 38 * 39 * This interface can be implemented to give instance method (instead of static method) versions of 40 * Settings.Global. It can be injected into class constructors and then faked or mocked as needed in 41 * tests. 42 * 43 * You can ask for [GlobalSettings] to be injected as needed. 44 * 45 * This class also provides [.registerContentObserver] methods, normally found on [ContentResolver] 46 * instances, unifying setting related actions in one place. 47 */ 48 public interface SettingsProxy { 49 /** Returns the [ContentResolver] this instance was constructed with. */ getContentResolvernull50 public fun getContentResolver(): ContentResolver 51 52 /** Returns the [CoroutineScope] that the async APIs will use. */ 53 public val settingsScope: CoroutineScope 54 55 @OptIn(ExperimentalStdlibApi::class) 56 public suspend fun executeOnSettingsScopeDispatcher(name: String, block: () -> Unit) { 57 val settingsDispatcher = settingsScope.coroutineContext[CoroutineDispatcher] 58 if ( 59 settingsDispatcher != null && 60 settingsDispatcher != coroutineContext[CoroutineDispatcher] 61 ) { 62 withContext(name, settingsDispatcher) { block() } 63 } else { 64 trace(name) { block() } 65 } 66 } 67 68 /** 69 * Construct the content URI for a particular name/value pair, useful for monitoring changes 70 * with a ContentObserver. 71 * 72 * @param name to look up in the table 73 * @return the corresponding content URI, or null if not present 74 */ getUriFornull75 @AnyThread public fun getUriFor(name: String): Uri 76 77 /** 78 * Registers listener for a given content observer <b>while blocking the current thread</b>. 79 * Implicitly calls [getUriFor] on the passed in name. 80 * 81 * This should not be called from the main thread, use [registerContentObserver] or 82 * [registerContentObserverAsync] instead. 83 */ 84 @WorkerThread 85 public fun registerContentObserverSync(name: String, settingsObserver: ContentObserver) { 86 registerContentObserverSync(getUriFor(name), settingsObserver) 87 } 88 89 /** 90 * Convenience wrapper around [ContentResolver.registerContentObserver].' 91 * 92 * suspend API corresponding to [registerContentObserver] to ensure that [ContentObserver] 93 * registration happens on a worker thread. Caller may wrap the API in an async block if they 94 * wish to synchronize execution. 95 */ registerContentObservernull96 public suspend fun registerContentObserver(name: String, settingsObserver: ContentObserver) { 97 executeOnSettingsScopeDispatcher("registerContentObserver-A") { 98 registerContentObserverSync(getUriFor(name), settingsObserver) 99 } 100 } 101 102 /** 103 * Convenience wrapper around [ContentResolver.registerContentObserver].' 104 * 105 * API corresponding to [registerContentObserver] for Java usage. 106 */ 107 @AnyThread registerContentObserverAsyncnull108 public fun registerContentObserverAsync(name: String, settingsObserver: ContentObserver): Job = 109 settingsScope.launch("registerContentObserverAsync-A") { 110 registerContentObserverSync(getUriFor(name), settingsObserver) 111 } 112 113 /** 114 * Convenience wrapper around [ContentResolver.registerContentObserver].' 115 * 116 * API corresponding to [registerContentObserver] for Java usage. After registration is 117 * complete, the callback block is called on the <b>background thread</b> to allow for update of 118 * value. 119 */ 120 @AnyThread registerContentObserverAsyncnull121 public fun registerContentObserverAsync( 122 name: String, 123 settingsObserver: ContentObserver, 124 @WorkerThread registered: Runnable, 125 ): Job = 126 settingsScope.launch("registerContentObserverAsync-B") { 127 registerContentObserverSync(getUriFor(name), settingsObserver) 128 registered.run() 129 } 130 131 /** 132 * Registers listener for a given content observer <b>while blocking the current thread</b>. 133 * 134 * This should not be called from the main thread, use [registerContentObserver] or 135 * [registerContentObserverAsync] instead. 136 */ 137 @WorkerThread registerContentObserverSyncnull138 public fun registerContentObserverSync(uri: Uri, settingsObserver: ContentObserver) { 139 registerContentObserverSync(uri, false, settingsObserver) 140 } 141 142 /** 143 * Convenience wrapper around [ContentResolver.registerContentObserver].' 144 * 145 * suspend API corresponding to [registerContentObserver] to ensure that [ContentObserver] 146 * registration happens on a worker thread. Caller may wrap the API in an async block if they 147 * wish to synchronize execution. 148 */ registerContentObservernull149 public suspend fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) { 150 executeOnSettingsScopeDispatcher("registerContentObserver-B") { 151 registerContentObserverSync(uri, settingsObserver) 152 } 153 } 154 155 /** 156 * Convenience wrapper around [ContentResolver.registerContentObserver].' 157 * 158 * API corresponding to [registerContentObserver] for Java usage. 159 */ 160 @AnyThread registerContentObserverAsyncnull161 public fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver): Job = 162 settingsScope.launch("registerContentObserverAsync-C") { 163 registerContentObserverSync(uri, settingsObserver) 164 } 165 166 /** 167 * Convenience wrapper around [ContentResolver.registerContentObserver].' 168 * 169 * API corresponding to [registerContentObserver] for Java usage. After registration is 170 * complete, the callback block is called on the <b>background thread</b> to allow for update of 171 * value. 172 */ 173 @AnyThread registerContentObserverAsyncnull174 public fun registerContentObserverAsync( 175 uri: Uri, 176 settingsObserver: ContentObserver, 177 @WorkerThread registered: Runnable, 178 ): Job = 179 settingsScope.launch("registerContentObserverAsync-D") { 180 registerContentObserverSync(uri, settingsObserver) 181 registered.run() 182 } 183 184 /** 185 * Convenience wrapper around [ContentResolver.registerContentObserver].' 186 * 187 * Implicitly calls [getUriFor] on the passed in name. 188 */ 189 @WorkerThread registerContentObserverSyncnull190 public fun registerContentObserverSync( 191 name: String, 192 notifyForDescendants: Boolean, 193 settingsObserver: ContentObserver, 194 ) { 195 registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver) 196 } 197 198 /** 199 * Convenience wrapper around [ContentResolver.registerContentObserver].' 200 * 201 * suspend API corresponding to [registerContentObserver] to ensure that [ContentObserver] 202 * registration happens on a worker thread. Caller may wrap the API in an async block if they 203 * wish to synchronize execution. 204 */ registerContentObservernull205 public suspend fun registerContentObserver( 206 name: String, 207 notifyForDescendants: Boolean, 208 settingsObserver: ContentObserver, 209 ) { 210 executeOnSettingsScopeDispatcher("registerContentObserver-C") { 211 registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver) 212 } 213 } 214 215 /** 216 * Convenience wrapper around [ContentResolver.registerContentObserver].' 217 * 218 * API corresponding to [registerContentObserver] for Java usage. 219 */ 220 @AnyThread registerContentObserverAsyncnull221 public fun registerContentObserverAsync( 222 name: String, 223 notifyForDescendants: Boolean, 224 settingsObserver: ContentObserver, 225 ): Job = 226 settingsScope.launch("registerContentObserverAsync-E") { 227 registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver) 228 } 229 230 /** 231 * Convenience wrapper around [ContentResolver.registerContentObserver].' 232 * 233 * API corresponding to [registerContentObserver] for Java usage. After registration is 234 * complete, the callback block is called on the <b>background thread</b> to allow for update of 235 * value. 236 */ 237 @AnyThread registerContentObserverAsyncnull238 public fun registerContentObserverAsync( 239 name: String, 240 notifyForDescendants: Boolean, 241 settingsObserver: ContentObserver, 242 @WorkerThread registered: Runnable, 243 ): Job = 244 settingsScope.launch("registerContentObserverAsync-F") { 245 registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver) 246 registered.run() 247 } 248 249 /** 250 * Registers listener for a given content observer <b>while blocking the current thread</b>. 251 * 252 * This should not be called from the main thread, use [registerContentObserver] or 253 * [registerContentObserverAsync] instead. 254 */ 255 @WorkerThread registerContentObserverSyncnull256 public fun registerContentObserverSync( 257 uri: Uri, 258 notifyForDescendants: Boolean, 259 settingsObserver: ContentObserver, 260 ) { 261 trace({ "SP#registerObserver#[$uri]" }) { 262 getContentResolver() 263 .registerContentObserver(uri, notifyForDescendants, settingsObserver) 264 } 265 } 266 267 /** 268 * Convenience wrapper around [ContentResolver.registerContentObserver].' 269 * 270 * suspend API corresponding to [registerContentObserver] to ensure that [ContentObserver] 271 * registration happens on a worker thread. Caller may wrap the API in an async block if they 272 * wish to synchronize execution. 273 */ registerContentObservernull274 public suspend fun registerContentObserver( 275 uri: Uri, 276 notifyForDescendants: Boolean, 277 settingsObserver: ContentObserver, 278 ) { 279 executeOnSettingsScopeDispatcher("registerContentObserver-D") { 280 registerContentObserverSync(uri, notifyForDescendants, settingsObserver) 281 } 282 } 283 284 /** 285 * Convenience wrapper around [ContentResolver.registerContentObserver].' 286 * 287 * API corresponding to [registerContentObserver] for Java usage. 288 */ 289 @AnyThread registerContentObserverAsyncnull290 public fun registerContentObserverAsync( 291 uri: Uri, 292 notifyForDescendants: Boolean, 293 settingsObserver: ContentObserver, 294 ): Job = 295 settingsScope.launch("registerContentObserverAsync-G") { 296 registerContentObserverSync(uri, notifyForDescendants, settingsObserver) 297 } 298 299 /** 300 * Convenience wrapper around [ContentResolver.registerContentObserver].' 301 * 302 * API corresponding to [registerContentObserver] for Java usage. After registration is 303 * complete, the callback block is called on the <b>background thread</b> to allow for update of 304 * value. 305 */ 306 @AnyThread registerContentObserverAsyncnull307 public fun registerContentObserverAsync( 308 uri: Uri, 309 notifyForDescendants: Boolean, 310 settingsObserver: ContentObserver, 311 @WorkerThread registered: Runnable, 312 ): Job = 313 settingsScope.launch("registerContentObserverAsync-H") { 314 registerContentObserverSync(uri, notifyForDescendants, settingsObserver) 315 registered.run() 316 } 317 318 /** 319 * Unregisters the given content observer <b>while blocking the current thread</b>. 320 * 321 * This should not be called from the main thread, use [unregisterContentObserver] or 322 * [unregisterContentObserverAsync] instead. 323 */ 324 @WorkerThread unregisterContentObserverSyncnull325 public fun unregisterContentObserverSync(settingsObserver: ContentObserver) { 326 trace({ "SP#unregisterObserver" }) { 327 getContentResolver().unregisterContentObserver(settingsObserver) 328 } 329 } 330 331 /** 332 * Convenience wrapper around [ContentResolver.unregisterContentObserver].' 333 * 334 * API corresponding to [unregisterContentObserver] for Java usage to ensure that 335 * [ContentObserver] un-registration happens on a worker thread. Caller may wrap the API in an 336 * async block if they wish to synchronize execution. 337 */ unregisterContentObservernull338 public suspend fun unregisterContentObserver(settingsObserver: ContentObserver) { 339 executeOnSettingsScopeDispatcher("unregisterContentObserver") { 340 unregisterContentObserverSync(settingsObserver) 341 } 342 } 343 344 /** 345 * Convenience wrapper around [ContentResolver.unregisterContentObserver].' 346 * 347 * API corresponding to [unregisterContentObserver] for Java usage to ensure that 348 * [ContentObserver] registration happens on a worker thread. 349 */ 350 @AnyThread unregisterContentObserverAsyncnull351 public fun unregisterContentObserverAsync(settingsObserver: ContentObserver): Job = 352 settingsScope.launch("unregisterContentObserverAsync") { 353 unregisterContentObserver(settingsObserver) 354 } 355 356 /** 357 * Look up a name in the database. 358 * 359 * @param name to look up in the table 360 * @return the corresponding value, or null if not present 361 */ getStringnull362 public fun getString(name: String): String? 363 364 /** 365 * Store a name/value pair into the database. 366 * 367 * @param name to store 368 * @param value to associate with the name 369 * @return true if the value was set, false on database errors 370 */ 371 public fun putString(name: String, value: String?): Boolean 372 373 /** 374 * Store a name/value pair into the database. 375 * 376 * The method takes an optional tag to associate with the setting which can be used to clear 377 * only settings made by your package and associated with this tag by passing the tag to 378 * [ ][.resetToDefaults]. Anyone can override the current tag. Also if another package changes 379 * the setting then the tag will be set to the one specified in the set call which can be null. 380 * Also any of the settings setters that do not take a tag as an argument effectively clears the 381 * tag. 382 * 383 * For example, if you set settings A and B with tags T1 and T2 and another app changes setting 384 * A (potentially to the same value), it can assign to it a tag T3 (note that now the package 385 * that changed the setting is not yours). Now if you reset your changes for T1 and T2 only 386 * setting B will be reset and A not (as it was changed by another package) but since A did not 387 * change you are in the desired initial state. Now if the other app changes the value of A 388 * (assuming you registered an observer in the beginning) you would detect that the setting was 389 * changed by another app and handle this appropriately (ignore, set back to some value, etc). 390 * 391 * Also the method takes an argument whether to make the value the default for this setting. If 392 * the system already specified a default value, then the one passed in here will **not** be set 393 * as the default. 394 * 395 * @param name to store. 396 * @param value to associate with the name. 397 * @param tag to associate with the setting. 398 * @param makeDefault whether to make the value the default one. 399 * @return true if the value was set, false on database errors. 400 * @see .resetToDefaults 401 */ 402 public fun putString(name: String, value: String?, tag: String?, makeDefault: Boolean): Boolean 403 404 /** 405 * Convenience function for retrieving a single secure settings value as an integer. Note that 406 * internally setting values are always stored as strings; this function converts the string to 407 * an integer for you. The default value will be returned if the setting is not defined or not 408 * an integer. 409 * 410 * @param name The name of the setting to retrieve. 411 * @param default Value to return if the setting is not defined. 412 * @return The setting's current value, or default if it is not defined or not a valid integer. 413 */ 414 public fun getInt(name: String, default: Int): Int { 415 val v = getString(name) 416 return try { 417 v?.toInt() ?: default 418 } catch (e: NumberFormatException) { 419 default 420 } 421 } 422 423 /** 424 * Convenience function for retrieving a single secure settings value as an integer. Note that 425 * internally setting values are always stored as strings; this function converts the string to 426 * an integer for you. 427 * 428 * This version does not take a default value. If the setting has not been set, or the string 429 * value is not a number, it throws [Settings.SettingNotFoundException]. 430 * 431 * @param name The name of the setting to retrieve. 432 * @return The setting's current value. 433 * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be 434 * found or the setting value is not an integer. 435 */ 436 @Throws(SettingNotFoundException::class) getIntnull437 public fun getInt(name: String): Int { 438 val v = getString(name) ?: throw SettingNotFoundException(name) 439 return try { 440 v.toInt() 441 } catch (e: NumberFormatException) { 442 throw SettingNotFoundException(name) 443 } 444 } 445 446 /** 447 * Convenience function for updating a single settings value as an integer. This will either 448 * create a new entry in the table if the given name does not exist, or modify the value of the 449 * existing row with that name. Note that internally setting values are always stored as 450 * strings, so this function converts the given value to a string before storing it. 451 * 452 * @param name The name of the setting to modify. 453 * @param value The new value for the setting. 454 * @return true if the value was set, false on database errors 455 */ putIntnull456 public fun putInt(name: String, value: Int): Boolean { 457 return putString(name, value.toString()) 458 } 459 460 /** 461 * Convenience function for retrieving a single secure settings value as a boolean. Note that 462 * internally setting values are always stored as strings; this function converts the string to 463 * a boolean for you. The default value will be returned if the setting is not defined or not a 464 * boolean. 465 * 466 * @param name The name of the setting to retrieve. 467 * @param default Value to return if the setting is not defined. 468 * @return The setting's current value, or default if it is not defined or not a valid boolean. 469 */ getBoolnull470 public fun getBool(name: String, default: Boolean): Boolean { 471 return getInt(name, if (default) 1 else 0) != 0 472 } 473 474 /** 475 * Convenience function for retrieving a single secure settings value as a boolean. Note that 476 * internally setting values are always stored as strings; this function converts the string to 477 * a boolean for you. 478 * 479 * This version does not take a default value. If the setting has not been set, or the string 480 * value is not a number, it throws [Settings.SettingNotFoundException]. 481 * 482 * @param name The name of the setting to retrieve. 483 * @return The setting's current value. 484 * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be 485 * found or the setting value is not a boolean. 486 */ 487 @Throws(SettingNotFoundException::class) getBoolnull488 public fun getBool(name: String): Boolean { 489 return getInt(name) != 0 490 } 491 492 /** 493 * Convenience function for updating a single settings value as a boolean. This will either 494 * create a new entry in the table if the given name does not exist, or modify the value of the 495 * existing row with that name. Note that internally setting values are always stored as 496 * strings, so this function converts the given value to a string before storing it. 497 * 498 * @param name The name of the setting to modify. 499 * @param value The new value for the setting. 500 * @return true if the value was set, false on database errors 501 */ putBoolnull502 public fun putBool(name: String, value: Boolean): Boolean { 503 return putInt(name, if (value) 1 else 0) 504 } 505 506 /** 507 * Convenience function for retrieving a single secure settings value as a `long`. Note that 508 * internally setting values are always stored as strings; this function converts the string to 509 * a `long` for you. The default value will be returned if the setting is not defined or not a 510 * `long`. 511 * 512 * @param name The name of the setting to retrieve. 513 * @param def Value to return if the setting is not defined. 514 * @return The setting's current value, or 'def' if it is not defined or not a valid `long`. 515 */ getLongnull516 public fun getLong(name: String, def: Long): Long { 517 val valString = getString(name) 518 return parseLongOrUseDefault(valString, def) 519 } 520 521 /** 522 * Convenience function for retrieving a single secure settings value as a `long`. Note that 523 * internally setting values are always stored as strings; this function converts the string to 524 * a `long` for you. 525 * 526 * This version does not take a default value. If the setting has not been set, or the string 527 * value is not a number, it throws [Settings.SettingNotFoundException]. 528 * 529 * @param name The name of the setting to retrieve. 530 * @return The setting's current value. 531 * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be 532 * found or the setting value is not an integer. 533 */ 534 @Throws(SettingNotFoundException::class) getLongnull535 public fun getLong(name: String): Long { 536 val valString = getString(name) 537 return parseLongOrThrow(name, valString) 538 } 539 540 /** 541 * Convenience function for updating a secure settings value as a long integer. This will either 542 * create a new entry in the table if the given name does not exist, or modify the value of the 543 * existing row with that name. Note that internally setting values are always stored as 544 * strings, so this function converts the given value to a string before storing it. 545 * 546 * @param name The name of the setting to modify. 547 * @param value The new value for the setting. 548 * @return true if the value was set, false on database errors 549 */ putLongnull550 public fun putLong(name: String, value: Long): Boolean { 551 return putString(name, value.toString()) 552 } 553 554 /** 555 * Convenience function for retrieving a single secure settings value as a floating point 556 * number. Note that internally setting values are always stored as strings; this function 557 * converts the string to an float for you. The default value will be returned if the setting is 558 * not defined or not a valid float. 559 * 560 * @param name The name of the setting to retrieve. 561 * @param def Value to return if the setting is not defined. 562 * @return The setting's current value, or 'def' if it is not defined or not a valid float. 563 */ getFloatnull564 public fun getFloat(name: String, def: Float): Float { 565 val v = getString(name) 566 return parseFloat(v, def) 567 } 568 569 /** 570 * Convenience function for retrieving a single secure settings value as a float. Note that 571 * internally setting values are always stored as strings; this function converts the string to 572 * a float for you. 573 * 574 * This version does not take a default value. If the setting has not been set, or the string 575 * value is not a number, it throws [Settings.SettingNotFoundException]. 576 * 577 * @param name The name of the setting to retrieve. 578 * @return The setting's current value. 579 * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be 580 * found or the setting value is not a float. 581 */ 582 @Throws(SettingNotFoundException::class) getFloatnull583 public fun getFloat(name: String): Float { 584 val v = getString(name) 585 return parseFloatOrThrow(name, v) 586 } 587 588 /** 589 * Convenience function for updating a single settings value as a floating point number. This 590 * will either create a new entry in the table if the given name does not exist, or modify the 591 * value of the existing row with that name. Note that internally setting values are always 592 * stored as strings, so this function converts the given value to a string before storing it. 593 * 594 * @param name The name of the setting to modify. 595 * @param value The new value for the setting. 596 * @return true if the value was set, false on database errors 597 */ putFloatnull598 public fun putFloat(name: String, value: Float): Boolean { 599 return putString(name, value.toString()) 600 } 601 602 public companion object { 603 /** Convert a string to a long, or uses a default if the string is malformed or null */ 604 @JvmStatic parseLongOrUseDefaultnull605 public fun parseLongOrUseDefault(valString: String?, default: Long): Long { 606 val value: Long = 607 try { 608 valString?.toLong() ?: default 609 } catch (e: NumberFormatException) { 610 default 611 } 612 return value 613 } 614 615 /** Convert a string to a long, or throws an exception if the string is malformed or null */ 616 @JvmStatic 617 @Throws(SettingNotFoundException::class) parseLongOrThrownull618 public fun parseLongOrThrow(name: String, valString: String?): Long { 619 if (valString == null) { 620 throw SettingNotFoundException(name) 621 } 622 return try { 623 valString.toLong() 624 } catch (e: NumberFormatException) { 625 throw SettingNotFoundException(name) 626 } 627 } 628 629 /** Convert a string to a float, or uses a default if the string is malformed or null */ 630 @JvmStatic parseFloatnull631 public fun parseFloat(v: String?, def: Float): Float { 632 return try { 633 v?.toFloat() ?: def 634 } catch (e: NumberFormatException) { 635 def 636 } 637 } 638 639 /** 640 * Convert a string to a float, or throws an exception if the string is malformed or null 641 */ 642 @JvmStatic 643 @Throws(SettingNotFoundException::class) parseFloatOrThrownull644 public fun parseFloatOrThrow(name: String, v: String?): Float { 645 if (v == null) { 646 throw SettingNotFoundException(name) 647 } 648 return try { 649 v.toFloat() 650 } catch (e: NumberFormatException) { 651 throw SettingNotFoundException(name) 652 } 653 } 654 } 655 656 public fun interface CurrentUserIdProvider { getUserIdnull657 @UserIdInt public fun getUserId(): Int 658 } 659 } 660