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