1/* 2 * Copyright (C) 2014 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 17package android.support.v4.app; 18 19import android.app.Notification; 20import android.app.NotificationManager; 21import android.app.Service; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.content.ServiceConnection; 26import android.content.pm.PackageManager; 27import android.content.pm.ResolveInfo; 28import android.os.Build; 29import android.os.Bundle; 30import android.os.DeadObjectException; 31import android.os.Handler; 32import android.os.HandlerThread; 33import android.os.IBinder; 34import android.os.Message; 35import android.os.RemoteException; 36import android.provider.Settings; 37import android.support.v4.os.BuildCompat; 38import android.util.Log; 39 40import java.util.HashMap; 41import java.util.HashSet; 42import java.util.Iterator; 43import java.util.LinkedList; 44import java.util.List; 45import java.util.Map; 46import java.util.Set; 47 48/** 49 * Compatibility library for NotificationManager with fallbacks for older platforms. 50 * 51 * <p>To use this class, call the static function {@link #from} to get a 52 * {@link NotificationManagerCompat} object, and then call one of its 53 * methods to post or cancel notifications. 54 */ 55public final class NotificationManagerCompat { 56 private static final String TAG = "NotifManCompat"; 57 58 /** 59 * Notification extras key: if set to true, the posted notification should use 60 * the side channel for delivery instead of using notification manager. 61 */ 62 public static final String EXTRA_USE_SIDE_CHANNEL = 63 NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL; 64 65 /** 66 * Intent action to register for on a service to receive side channel 67 * notifications. The listening service must be in the same package as an enabled 68 * {@link android.service.notification.NotificationListenerService}. 69 */ 70 public static final String ACTION_BIND_SIDE_CHANNEL = 71 "android.support.BIND_NOTIFICATION_SIDE_CHANNEL"; 72 73 /** 74 * Maximum sdk build version which needs support for side channeled notifications. 75 * Currently the only needed use is for side channeling group children before KITKAT_WATCH. 76 */ 77 static final int MAX_SIDE_CHANNEL_SDK_VERSION = 19; 78 79 /** Base time delay for a side channel listener queue retry. */ 80 private static final int SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS = 1000; 81 /** Maximum retries for a side channel listener before dropping tasks. */ 82 private static final int SIDE_CHANNEL_RETRY_MAX_COUNT = 6; 83 /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ 84 private static final String SETTING_ENABLED_NOTIFICATION_LISTENERS = 85 "enabled_notification_listeners"; 86 static final int SIDE_CHANNEL_BIND_FLAGS; 87 88 /** Cache of enabled notification listener components */ 89 private static final Object sEnabledNotificationListenersLock = new Object(); 90 /** Guarded by {@link #sEnabledNotificationListenersLock} */ 91 private static String sEnabledNotificationListeners; 92 /** Guarded by {@link #sEnabledNotificationListenersLock} */ 93 private static Set<String> sEnabledNotificationListenerPackages = new HashSet<String>(); 94 95 private final Context mContext; 96 private final NotificationManager mNotificationManager; 97 /** Lock for mutable static fields */ 98 private static final Object sLock = new Object(); 99 /** Guarded by {@link #sLock} */ 100 private static SideChannelManager sSideChannelManager; 101 102 /** 103 * Value signifying that the user has not expressed an importance. 104 * 105 * This value is for persisting preferences, and should never be associated with 106 * an actual notification. 107 */ 108 public static final int IMPORTANCE_UNSPECIFIED = -1000; 109 110 /** 111 * A notification with no importance: shows nowhere, is blocked. 112 */ 113 public static final int IMPORTANCE_NONE = 0; 114 115 /** 116 * Min notification importance: only shows in the shade, below the fold. 117 */ 118 public static final int IMPORTANCE_MIN = 1; 119 120 /** 121 * Low notification importance: shows everywhere, but is not intrusive. 122 */ 123 public static final int IMPORTANCE_LOW = 2; 124 125 /** 126 * Default notification importance: shows everywhere, allowed to makes noise, 127 * but does not visually intrude. 128 */ 129 public static final int IMPORTANCE_DEFAULT = 3; 130 131 /** 132 * Higher notification importance: shows everywhere, allowed to makes noise and peek. 133 */ 134 public static final int IMPORTANCE_HIGH = 4; 135 136 /** 137 * Highest notification importance: shows everywhere, allowed to makes noise, peek, and 138 * use full screen intents. 139 */ 140 public static final int IMPORTANCE_MAX = 5; 141 142 /** Get a {@link NotificationManagerCompat} instance for a provided context. */ 143 public static NotificationManagerCompat from(Context context) { 144 return new NotificationManagerCompat(context); 145 } 146 147 private NotificationManagerCompat(Context context) { 148 mContext = context; 149 mNotificationManager = (NotificationManager) mContext.getSystemService( 150 Context.NOTIFICATION_SERVICE); 151 } 152 153 private static final Impl IMPL; 154 155 interface Impl { 156 void cancelNotification(NotificationManager notificationManager, String tag, int id); 157 158 void postNotification(NotificationManager notificationManager, String tag, int id, 159 Notification notification); 160 161 int getSideChannelBindFlags(); 162 163 boolean areNotificationsEnabled(Context context, NotificationManager notificationManager); 164 165 int getImportance(NotificationManager notificationManager); 166 } 167 168 static class ImplBase implements Impl { 169 170 @Override 171 public void cancelNotification(NotificationManager notificationManager, String tag, 172 int id) { 173 notificationManager.cancel(tag, id); 174 } 175 176 @Override 177 public void postNotification(NotificationManager notificationManager, String tag, int id, 178 Notification notification) { 179 notificationManager.notify(tag, id, notification); 180 } 181 182 @Override 183 public int getSideChannelBindFlags() { 184 return Service.BIND_AUTO_CREATE; 185 } 186 187 @Override 188 public boolean areNotificationsEnabled(Context context, 189 NotificationManager notificationManager) { 190 return true; 191 } 192 193 @Override 194 public int getImportance(NotificationManager notificationManager) { 195 return IMPORTANCE_UNSPECIFIED; 196 } 197 } 198 199 static class ImplIceCreamSandwich extends ImplBase { 200 @Override 201 public int getSideChannelBindFlags() { 202 return NotificationManagerCompatIceCreamSandwich.SIDE_CHANNEL_BIND_FLAGS; 203 } 204 } 205 206 static class ImplKitKat extends ImplIceCreamSandwich { 207 @Override 208 public boolean areNotificationsEnabled(Context context, 209 NotificationManager notificationManager) { 210 return NotificationManagerCompatKitKat.areNotificationsEnabled(context); 211 } 212 } 213 214 static class ImplApi24 extends ImplKitKat { 215 @Override 216 public boolean areNotificationsEnabled(Context context, 217 NotificationManager notificationManager) { 218 return NotificationManagerCompatApi24.areNotificationsEnabled(notificationManager); 219 } 220 221 @Override 222 public int getImportance(NotificationManager notificationManager) { 223 return NotificationManagerCompatApi24.getImportance(notificationManager); 224 } 225 } 226 227 static { 228 if (BuildCompat.isAtLeastN()) { 229 IMPL = new ImplApi24(); 230 } else if (Build.VERSION.SDK_INT >= 19) { 231 IMPL = new ImplKitKat(); 232 } else if (Build.VERSION.SDK_INT >= 14) { 233 IMPL = new ImplIceCreamSandwich(); 234 } else { 235 IMPL = new ImplBase(); 236 } 237 SIDE_CHANNEL_BIND_FLAGS = IMPL.getSideChannelBindFlags(); 238 } 239 240 /** 241 * Cancel a previously shown notification. 242 * @param id the ID of the notification 243 */ 244 public void cancel(int id) { 245 cancel(null, id); 246 } 247 248 /** 249 * Cancel a previously shown notification. 250 * @param tag the string identifier of the notification. 251 * @param id the ID of the notification 252 */ 253 public void cancel(String tag, int id) { 254 IMPL.cancelNotification(mNotificationManager, tag, id); 255 if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { 256 pushSideChannelQueue(new CancelTask(mContext.getPackageName(), id, tag)); 257 } 258 } 259 260 /** Cancel all previously shown notifications. */ 261 public void cancelAll() { 262 mNotificationManager.cancelAll(); 263 if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { 264 pushSideChannelQueue(new CancelTask(mContext.getPackageName())); 265 } 266 } 267 268 /** 269 * Post a notification to be shown in the status bar, stream, etc. 270 * @param id the ID of the notification 271 * @param notification the notification to post to the system 272 */ 273 public void notify(int id, Notification notification) { 274 notify(null, id, notification); 275 } 276 277 /** 278 * Post a notification to be shown in the status bar, stream, etc. 279 * @param tag the string identifier for a notification. Can be {@code null}. 280 * @param id the ID of the notification. The pair (tag, id) must be unique within your app. 281 * @param notification the notification to post to the system 282 */ 283 public void notify(String tag, int id, Notification notification) { 284 if (useSideChannelForNotification(notification)) { 285 pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification)); 286 // Cancel this notification in notification manager if it just transitioned to being 287 // side channelled. 288 IMPL.cancelNotification(mNotificationManager, tag, id); 289 } else { 290 IMPL.postNotification(mNotificationManager, tag, id, notification); 291 } 292 } 293 294 /** 295 * Returns whether notifications from the calling package are not blocked. 296 */ 297 public boolean areNotificationsEnabled() { 298 return IMPL.areNotificationsEnabled(mContext, mNotificationManager); 299 } 300 301 /** 302 * Returns the user specified importance for notifications from the calling package. 303 * 304 * @return An importance level, such as {@link #IMPORTANCE_DEFAULT}. 305 */ 306 public int getImportance() { 307 return IMPL.getImportance(mNotificationManager); 308 } 309 310 /** 311 * Get the set of packages that have an enabled notification listener component within them. 312 */ 313 public static Set<String> getEnabledListenerPackages(Context context) { 314 final String enabledNotificationListeners = Settings.Secure.getString( 315 context.getContentResolver(), 316 SETTING_ENABLED_NOTIFICATION_LISTENERS); 317 synchronized (sEnabledNotificationListenersLock) { 318 // Parse the string again if it is different from the last time this method was called. 319 if (enabledNotificationListeners != null 320 && !enabledNotificationListeners.equals(sEnabledNotificationListeners)) { 321 final String[] components = enabledNotificationListeners.split(":"); 322 Set<String> packageNames = new HashSet<String>(components.length); 323 for (String component : components) { 324 ComponentName componentName = ComponentName.unflattenFromString(component); 325 if (componentName != null) { 326 packageNames.add(componentName.getPackageName()); 327 } 328 } 329 sEnabledNotificationListenerPackages = packageNames; 330 sEnabledNotificationListeners = enabledNotificationListeners; 331 } 332 return sEnabledNotificationListenerPackages; 333 } 334 } 335 336 /** 337 * Returns true if this notification should use the side channel for delivery. 338 */ 339 private static boolean useSideChannelForNotification(Notification notification) { 340 Bundle extras = NotificationCompat.getExtras(notification); 341 return extras != null && extras.getBoolean(EXTRA_USE_SIDE_CHANNEL); 342 } 343 344 /** 345 * Push a notification task for distribution to notification side channels. 346 */ 347 private void pushSideChannelQueue(Task task) { 348 synchronized (sLock) { 349 if (sSideChannelManager == null) { 350 sSideChannelManager = new SideChannelManager(mContext.getApplicationContext()); 351 } 352 sSideChannelManager.queueTask(task); 353 } 354 } 355 356 /** 357 * Helper class to manage a queue of pending tasks to send to notification side channel 358 * listeners. 359 */ 360 private static class SideChannelManager implements Handler.Callback, ServiceConnection { 361 private static final int MSG_QUEUE_TASK = 0; 362 private static final int MSG_SERVICE_CONNECTED = 1; 363 private static final int MSG_SERVICE_DISCONNECTED = 2; 364 private static final int MSG_RETRY_LISTENER_QUEUE = 3; 365 366 private static final String KEY_BINDER = "binder"; 367 368 private final Context mContext; 369 private final HandlerThread mHandlerThread; 370 private final Handler mHandler; 371 private final Map<ComponentName, ListenerRecord> mRecordMap = 372 new HashMap<ComponentName, ListenerRecord>(); 373 private Set<String> mCachedEnabledPackages = new HashSet<String>(); 374 375 public SideChannelManager(Context context) { 376 mContext = context; 377 mHandlerThread = new HandlerThread("NotificationManagerCompat"); 378 mHandlerThread.start(); 379 mHandler = new Handler(mHandlerThread.getLooper(), this); 380 } 381 382 /** 383 * Queue a new task to be sent to all listeners. This function can be called 384 * from any thread. 385 */ 386 public void queueTask(Task task) { 387 mHandler.obtainMessage(MSG_QUEUE_TASK, task).sendToTarget(); 388 } 389 390 @Override 391 public boolean handleMessage(Message msg) { 392 switch (msg.what) { 393 case MSG_QUEUE_TASK: 394 handleQueueTask((Task) msg.obj); 395 return true; 396 case MSG_SERVICE_CONNECTED: 397 ServiceConnectedEvent event = (ServiceConnectedEvent) msg.obj; 398 handleServiceConnected(event.componentName, event.iBinder); 399 return true; 400 case MSG_SERVICE_DISCONNECTED: 401 handleServiceDisconnected((ComponentName) msg.obj); 402 return true; 403 case MSG_RETRY_LISTENER_QUEUE: 404 handleRetryListenerQueue((ComponentName) msg.obj); 405 return true; 406 } 407 return false; 408 } 409 410 private void handleQueueTask(Task task) { 411 updateListenerMap(); 412 for (ListenerRecord record : mRecordMap.values()) { 413 record.taskQueue.add(task); 414 processListenerQueue(record); 415 } 416 } 417 418 private void handleServiceConnected(ComponentName componentName, IBinder iBinder) { 419 ListenerRecord record = mRecordMap.get(componentName); 420 if (record != null) { 421 record.service = INotificationSideChannel.Stub.asInterface(iBinder); 422 record.retryCount = 0; 423 processListenerQueue(record); 424 } 425 } 426 427 private void handleServiceDisconnected(ComponentName componentName) { 428 ListenerRecord record = mRecordMap.get(componentName); 429 if (record != null) { 430 ensureServiceUnbound(record); 431 } 432 } 433 434 private void handleRetryListenerQueue(ComponentName componentName) { 435 ListenerRecord record = mRecordMap.get(componentName); 436 if (record != null) { 437 processListenerQueue(record); 438 } 439 } 440 441 @Override 442 public void onServiceConnected(ComponentName componentName, IBinder iBinder) { 443 if (Log.isLoggable(TAG, Log.DEBUG)) { 444 Log.d(TAG, "Connected to service " + componentName); 445 } 446 mHandler.obtainMessage(MSG_SERVICE_CONNECTED, 447 new ServiceConnectedEvent(componentName, iBinder)) 448 .sendToTarget(); 449 } 450 451 @Override 452 public void onServiceDisconnected(ComponentName componentName) { 453 if (Log.isLoggable(TAG, Log.DEBUG)) { 454 Log.d(TAG, "Disconnected from service " + componentName); 455 } 456 mHandler.obtainMessage(MSG_SERVICE_DISCONNECTED, componentName).sendToTarget(); 457 } 458 459 /** 460 * Check the current list of enabled listener packages and update the records map 461 * accordingly. 462 */ 463 private void updateListenerMap() { 464 Set<String> enabledPackages = getEnabledListenerPackages(mContext); 465 if (enabledPackages.equals(mCachedEnabledPackages)) { 466 // Short-circuit when the list of enabled packages has not changed. 467 return; 468 } 469 mCachedEnabledPackages = enabledPackages; 470 List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServices( 471 new Intent().setAction(ACTION_BIND_SIDE_CHANNEL), PackageManager.GET_SERVICES); 472 Set<ComponentName> enabledComponents = new HashSet<ComponentName>(); 473 for (ResolveInfo resolveInfo : resolveInfos) { 474 if (!enabledPackages.contains(resolveInfo.serviceInfo.packageName)) { 475 continue; 476 } 477 ComponentName componentName = new ComponentName( 478 resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); 479 if (resolveInfo.serviceInfo.permission != null) { 480 Log.w(TAG, "Permission present on component " + componentName 481 + ", not adding listener record."); 482 continue; 483 } 484 enabledComponents.add(componentName); 485 } 486 // Ensure all enabled components have a record in the listener map. 487 for (ComponentName componentName : enabledComponents) { 488 if (!mRecordMap.containsKey(componentName)) { 489 if (Log.isLoggable(TAG, Log.DEBUG)) { 490 Log.d(TAG, "Adding listener record for " + componentName); 491 } 492 mRecordMap.put(componentName, new ListenerRecord(componentName)); 493 } 494 } 495 // Remove listener records that are no longer for enabled components. 496 Iterator<Map.Entry<ComponentName, ListenerRecord>> it = 497 mRecordMap.entrySet().iterator(); 498 while (it.hasNext()) { 499 Map.Entry<ComponentName, ListenerRecord> entry = it.next(); 500 if (!enabledComponents.contains(entry.getKey())) { 501 if (Log.isLoggable(TAG, Log.DEBUG)) { 502 Log.d(TAG, "Removing listener record for " + entry.getKey()); 503 } 504 ensureServiceUnbound(entry.getValue()); 505 it.remove(); 506 } 507 } 508 } 509 510 /** 511 * Ensure we are already attempting to bind to a service, or start a new binding if not. 512 * @return Whether the service bind attempt was successful. 513 */ 514 private boolean ensureServiceBound(ListenerRecord record) { 515 if (record.bound) { 516 return true; 517 } 518 Intent intent = new Intent(ACTION_BIND_SIDE_CHANNEL).setComponent(record.componentName); 519 record.bound = mContext.bindService(intent, this, SIDE_CHANNEL_BIND_FLAGS); 520 if (record.bound) { 521 record.retryCount = 0; 522 } else { 523 Log.w(TAG, "Unable to bind to listener " + record.componentName); 524 mContext.unbindService(this); 525 } 526 return record.bound; 527 } 528 529 /** 530 * Ensure we have unbound from a service. 531 */ 532 private void ensureServiceUnbound(ListenerRecord record) { 533 if (record.bound) { 534 mContext.unbindService(this); 535 record.bound = false; 536 } 537 record.service = null; 538 } 539 540 /** 541 * Schedule a delayed retry to communicate with a listener service. 542 * After a maximum number of attempts (with exponential back-off), start 543 * dropping pending tasks for this listener. 544 */ 545 private void scheduleListenerRetry(ListenerRecord record) { 546 if (mHandler.hasMessages(MSG_RETRY_LISTENER_QUEUE, record.componentName)) { 547 return; 548 } 549 record.retryCount++; 550 if (record.retryCount > SIDE_CHANNEL_RETRY_MAX_COUNT) { 551 Log.w(TAG, "Giving up on delivering " + record.taskQueue.size() + " tasks to " 552 + record.componentName + " after " + record.retryCount + " retries"); 553 record.taskQueue.clear(); 554 return; 555 } 556 int delayMs = SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS * (1 << (record.retryCount - 1)); 557 if (Log.isLoggable(TAG, Log.DEBUG)) { 558 Log.d(TAG, "Scheduling retry for " + delayMs + " ms"); 559 } 560 Message msg = mHandler.obtainMessage(MSG_RETRY_LISTENER_QUEUE, record.componentName); 561 mHandler.sendMessageDelayed(msg, delayMs); 562 } 563 564 /** 565 * Perform a processing step for a listener. First check the bind state, then attempt 566 * to flush the task queue, and if an error is encountered, schedule a retry. 567 */ 568 private void processListenerQueue(ListenerRecord record) { 569 if (Log.isLoggable(TAG, Log.DEBUG)) { 570 Log.d(TAG, "Processing component " + record.componentName + ", " 571 + record.taskQueue.size() + " queued tasks"); 572 } 573 if (record.taskQueue.isEmpty()) { 574 return; 575 } 576 if (!ensureServiceBound(record) || record.service == null) { 577 // Ensure bind has started and that a service interface is ready to use. 578 scheduleListenerRetry(record); 579 return; 580 } 581 // Attempt to flush all items in the task queue. 582 while (true) { 583 Task task = record.taskQueue.peek(); 584 if (task == null) { 585 break; 586 } 587 try { 588 if (Log.isLoggable(TAG, Log.DEBUG)) { 589 Log.d(TAG, "Sending task " + task); 590 } 591 task.send(record.service); 592 record.taskQueue.remove(); 593 } catch (DeadObjectException e) { 594 if (Log.isLoggable(TAG, Log.DEBUG)) { 595 Log.d(TAG, "Remote service has died: " + record.componentName); 596 } 597 break; 598 } catch (RemoteException e) { 599 Log.w(TAG, "RemoteException communicating with " + record.componentName, e); 600 break; 601 } 602 } 603 if (!record.taskQueue.isEmpty()) { 604 // Some tasks were not sent, meaning an error was encountered, schedule a retry. 605 scheduleListenerRetry(record); 606 } 607 } 608 609 /** A per-side-channel-service listener state record */ 610 private static class ListenerRecord { 611 public final ComponentName componentName; 612 /** Whether the service is currently bound to. */ 613 public boolean bound = false; 614 /** The service stub provided by onServiceConnected */ 615 public INotificationSideChannel service; 616 /** Queue of pending tasks to send to this listener service */ 617 public LinkedList<Task> taskQueue = new LinkedList<Task>(); 618 /** Number of retries attempted while connecting to this listener service */ 619 public int retryCount = 0; 620 621 public ListenerRecord(ComponentName componentName) { 622 this.componentName = componentName; 623 } 624 } 625 } 626 627 private static class ServiceConnectedEvent { 628 final ComponentName componentName; 629 final IBinder iBinder; 630 631 public ServiceConnectedEvent(ComponentName componentName, 632 final IBinder iBinder) { 633 this.componentName = componentName; 634 this.iBinder = iBinder; 635 } 636 } 637 638 private interface Task { 639 public void send(INotificationSideChannel service) throws RemoteException; 640 } 641 642 private static class NotifyTask implements Task { 643 final String packageName; 644 final int id; 645 final String tag; 646 final Notification notif; 647 648 public NotifyTask(String packageName, int id, String tag, Notification notif) { 649 this.packageName = packageName; 650 this.id = id; 651 this.tag = tag; 652 this.notif = notif; 653 } 654 655 @Override 656 public void send(INotificationSideChannel service) throws RemoteException { 657 service.notify(packageName, id, tag, notif); 658 } 659 660 public String toString() { 661 StringBuilder sb = new StringBuilder("NotifyTask["); 662 sb.append("packageName:").append(packageName); 663 sb.append(", id:").append(id); 664 sb.append(", tag:").append(tag); 665 sb.append("]"); 666 return sb.toString(); 667 } 668 } 669 670 private static class CancelTask implements Task { 671 final String packageName; 672 final int id; 673 final String tag; 674 final boolean all; 675 676 public CancelTask(String packageName) { 677 this.packageName = packageName; 678 this.id = 0; 679 this.tag = null; 680 this.all = true; 681 } 682 683 public CancelTask(String packageName, int id, String tag) { 684 this.packageName = packageName; 685 this.id = id; 686 this.tag = tag; 687 this.all = false; 688 } 689 690 @Override 691 public void send(INotificationSideChannel service) throws RemoteException { 692 if (all) { 693 service.cancelAll(packageName); 694 } else { 695 service.cancel(packageName, id, tag); 696 } 697 } 698 699 public String toString() { 700 StringBuilder sb = new StringBuilder("CancelTask["); 701 sb.append("packageName:").append(packageName); 702 sb.append(", id:").append(id); 703 sb.append(", tag:").append(tag); 704 sb.append(", all:").append(all); 705 sb.append("]"); 706 return sb.toString(); 707 } 708 } 709} 710