• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 android.view.contentcapture;
17 
18 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
19 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
20 import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID;
21 
22 import android.annotation.CallSuper;
23 import android.annotation.IntDef;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.graphics.Insets;
27 import android.util.DebugUtils;
28 import android.util.Log;
29 import android.view.View;
30 import android.view.ViewStructure;
31 import android.view.autofill.AutofillId;
32 import android.view.contentcapture.ViewNode.ViewStructureImpl;
33 
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.ArrayUtils;
37 import com.android.internal.util.Preconditions;
38 
39 import java.io.PrintWriter;
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.security.SecureRandom;
43 import java.util.ArrayList;
44 import java.util.Objects;
45 
46 /**
47  * Session used when notifying the Android system about events associated with views.
48  */
49 public abstract class ContentCaptureSession implements AutoCloseable {
50 
51     private static final String TAG = ContentCaptureSession.class.getSimpleName();
52 
53     // TODO(b/158778794): to make the session ids truly globally unique across
54     //  processes, we may need to explore other options.
55     private static final SecureRandom ID_GENERATOR = new SecureRandom();
56 
57     /**
58      * Initial state, when there is no session.
59      *
60      * @hide
61      */
62     // NOTE: not prefixed by STATE_ so it's not printed on getStateAsString()
63     public static final int UNKNOWN_STATE = 0x0;
64 
65     /**
66      * Service's startSession() was called, but server didn't confirm it was created yet.
67      *
68      * @hide
69      */
70     public static final int STATE_WAITING_FOR_SERVER = 0x1;
71 
72     /**
73      * Session is active.
74      *
75      * @hide
76      */
77     public static final int STATE_ACTIVE = 0x2;
78 
79     /**
80      * Session is disabled because there is no service for this user.
81      *
82      * @hide
83      */
84     public static final int STATE_DISABLED = 0x4;
85 
86     /**
87      * Session is disabled because its id already existed on server.
88      *
89      * @hide
90      */
91     public static final int STATE_DUPLICATED_ID = 0x8;
92 
93     /**
94      * Session is disabled because service is not set for user.
95      *
96      * @hide
97      */
98     public static final int STATE_NO_SERVICE = 0x10;
99 
100     /**
101      * Session is disabled by FLAG_SECURE
102      *
103      * @hide
104      */
105     public static final int STATE_FLAG_SECURE = 0x20;
106 
107     /**
108      * Session is disabled manually by the specific app
109      * (through {@link ContentCaptureManager#setContentCaptureEnabled(boolean)}).
110      *
111      * @hide
112      */
113     public static final int STATE_BY_APP = 0x40;
114 
115     /**
116      * Session is disabled because session start was never replied.
117      *
118      * @hide
119      */
120     public static final int STATE_NO_RESPONSE = 0x80;
121 
122     /**
123      * Session is disabled because an internal error.
124      *
125      * @hide
126      */
127     public static final int STATE_INTERNAL_ERROR = 0x100;
128 
129     /**
130      * Session is disabled because service didn't allowlist package or activity.
131      *
132      * @hide
133      */
134     public static final int STATE_NOT_WHITELISTED = 0x200;
135 
136     /**
137      * Session is disabled because the service died.
138      *
139      * @hide
140      */
141     public static final int STATE_SERVICE_DIED = 0x400;
142 
143     /**
144      * Session is disabled because the service package is being udpated.
145      *
146      * @hide
147      */
148     public static final int STATE_SERVICE_UPDATING = 0x800;
149 
150     /**
151      * Session is enabled, after the service died and came back to live.
152      *
153      * @hide
154      */
155     public static final int STATE_SERVICE_RESURRECTED = 0x1000;
156 
157     private static final int INITIAL_CHILDREN_CAPACITY = 5;
158 
159     /** @hide */
160     public static final int FLUSH_REASON_FULL = 1;
161     /** @hide */
162     public static final int FLUSH_REASON_VIEW_ROOT_ENTERED = 2;
163     /** @hide */
164     public static final int FLUSH_REASON_SESSION_STARTED = 3;
165     /** @hide */
166     public static final int FLUSH_REASON_SESSION_FINISHED = 4;
167     /** @hide */
168     public static final int FLUSH_REASON_IDLE_TIMEOUT = 5;
169     /** @hide */
170     public static final int FLUSH_REASON_TEXT_CHANGE_TIMEOUT = 6;
171     /** @hide */
172     public static final int FLUSH_REASON_SESSION_CONNECTED = 7;
173     /** @hide */
174     public static final int FLUSH_REASON_FORCE_FLUSH = 8;
175     /** @hide */
176     public static final int FLUSH_REASON_VIEW_TREE_APPEARING = 9;
177     /** @hide */
178     public static final int FLUSH_REASON_VIEW_TREE_APPEARED = 10;
179 
180     /** @hide */
181     @IntDef(prefix = { "FLUSH_REASON_" }, value = {
182             FLUSH_REASON_FULL,
183             FLUSH_REASON_VIEW_ROOT_ENTERED,
184             FLUSH_REASON_SESSION_STARTED,
185             FLUSH_REASON_SESSION_FINISHED,
186             FLUSH_REASON_IDLE_TIMEOUT,
187             FLUSH_REASON_TEXT_CHANGE_TIMEOUT,
188             FLUSH_REASON_SESSION_CONNECTED,
189             FLUSH_REASON_FORCE_FLUSH,
190             FLUSH_REASON_VIEW_TREE_APPEARING,
191             FLUSH_REASON_VIEW_TREE_APPEARED
192     })
193     @Retention(RetentionPolicy.SOURCE)
194     public @interface FlushReason{}
195 
196     private final Object mLock = new Object();
197 
198     /**
199      * Guard use to ignore events after it's destroyed.
200      */
201     @NonNull
202     @GuardedBy("mLock")
203     private boolean mDestroyed;
204 
205     /** @hide */
206     @Nullable
207     protected final int mId;
208 
209     private int mState = UNKNOWN_STATE;
210 
211     // Lazily created on demand.
212     private ContentCaptureSessionId mContentCaptureSessionId;
213 
214     /**
215      * {@link ContentCaptureContext} set by client, or {@code null} when it's the
216      * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the
217      * context.
218      */
219     @Nullable
220     private ContentCaptureContext mClientContext;
221 
222     /**
223      * List of children session.
224      */
225     @Nullable
226     @GuardedBy("mLock")
227     private ArrayList<ContentCaptureSession> mChildren;
228 
229     /** @hide */
ContentCaptureSession()230     protected ContentCaptureSession() {
231         this(getRandomSessionId());
232     }
233 
234     /** @hide */
235     @VisibleForTesting
ContentCaptureSession(int id)236     public ContentCaptureSession(int id) {
237         Preconditions.checkArgument(id != NO_SESSION_ID);
238         mId = id;
239     }
240 
241     // Used by ChildCOntentCaptureSession
ContentCaptureSession(@onNull ContentCaptureContext initialContext)242     ContentCaptureSession(@NonNull ContentCaptureContext initialContext) {
243         this();
244         mClientContext = Objects.requireNonNull(initialContext);
245     }
246 
247     /** @hide */
248     @NonNull
getMainCaptureSession()249     abstract MainContentCaptureSession getMainCaptureSession();
250 
251     /**
252      * Gets the id used to identify this session.
253      */
254     @NonNull
getContentCaptureSessionId()255     public final ContentCaptureSessionId getContentCaptureSessionId() {
256         if (mContentCaptureSessionId == null) {
257             mContentCaptureSessionId = new ContentCaptureSessionId(mId);
258         }
259         return mContentCaptureSessionId;
260     }
261 
262     /** @hide */
263     @NonNull
getId()264     public int getId() {
265         return mId;
266     }
267 
268     /**
269      * Creates a new {@link ContentCaptureSession}.
270      *
271      * <p>See {@link View#setContentCaptureSession(ContentCaptureSession)} for more info.
272      */
273     @NonNull
createContentCaptureSession( @onNull ContentCaptureContext context)274     public final ContentCaptureSession createContentCaptureSession(
275             @NonNull ContentCaptureContext context) {
276         final ContentCaptureSession child = newChild(context);
277         if (sDebug) {
278             Log.d(TAG, "createContentCaptureSession(" + context + ": parent=" + mId + ", child="
279                     + child.mId);
280         }
281         synchronized (mLock) {
282             if (mChildren == null) {
283                 mChildren = new ArrayList<>(INITIAL_CHILDREN_CAPACITY);
284             }
285             mChildren.add(child);
286         }
287         return child;
288     }
289 
newChild(@onNull ContentCaptureContext context)290     abstract ContentCaptureSession newChild(@NonNull ContentCaptureContext context);
291 
292     /**
293      * Flushes the buffered events to the service.
294      */
flush(@lushReason int reason)295     abstract void flush(@FlushReason int reason);
296 
297     /**
298      * Sets the {@link ContentCaptureContext} associated with the session.
299      *
300      * <p>Typically used to change the context associated with the default session from an activity.
301      */
setContentCaptureContext(@ullable ContentCaptureContext context)302     public final void setContentCaptureContext(@Nullable ContentCaptureContext context) {
303         if (!isContentCaptureEnabled()) return;
304 
305         mClientContext = context;
306         updateContentCaptureContext(context);
307     }
308 
updateContentCaptureContext(@ullable ContentCaptureContext context)309     abstract void updateContentCaptureContext(@Nullable ContentCaptureContext context);
310 
311     /**
312      * Gets the {@link ContentCaptureContext} associated with the session.
313      *
314      * @return context set on constructor or by
315      *         {@link #setContentCaptureContext(ContentCaptureContext)}, or {@code null} if never
316      *         explicitly set.
317      */
318     @Nullable
getContentCaptureContext()319     public final ContentCaptureContext getContentCaptureContext() {
320         return mClientContext;
321     }
322 
323     /**
324      * Destroys this session, flushing out all pending notifications to the service.
325      *
326      * <p>Once destroyed, any new notification will be dropped.
327      */
destroy()328     public final void destroy() {
329         synchronized (mLock) {
330             if (mDestroyed) {
331                 if (sDebug) Log.d(TAG, "destroy(" + mId + "): already destroyed");
332                 return;
333             }
334             mDestroyed = true;
335 
336             // TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
337             // id) and send it to the cache of batched commands
338             if (sVerbose) {
339                 Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
340             }
341             // Finish children first
342             if (mChildren != null) {
343                 final int numberChildren = mChildren.size();
344                 if (sVerbose) Log.v(TAG, "Destroying " + numberChildren + " children first");
345                 for (int i = 0; i < numberChildren; i++) {
346                     final ContentCaptureSession child = mChildren.get(i);
347                     try {
348                         child.destroy();
349                     } catch (Exception e) {
350                         Log.w(TAG, "exception destroying child session #" + i + ": " + e);
351                     }
352                 }
353             }
354         }
355 
356         onDestroy();
357     }
358 
onDestroy()359     abstract void onDestroy();
360 
361     /** @hide */
362     @Override
close()363     public void close() {
364         destroy();
365     }
366 
367     /**
368      * Notifies the Content Capture Service that a node has been added to the view structure.
369      *
370      * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
371      * automatically by the Android System for views that return {@code true} on
372      * {@link View#onProvideContentCaptureStructure(ViewStructure, int)}.
373      *
374      * @param node node that has been added.
375      */
notifyViewAppeared(@onNull ViewStructure node)376     public final void notifyViewAppeared(@NonNull ViewStructure node) {
377         Objects.requireNonNull(node);
378         if (!isContentCaptureEnabled()) return;
379 
380         if (!(node instanceof ViewNode.ViewStructureImpl)) {
381             throw new IllegalArgumentException("Invalid node class: " + node.getClass());
382         }
383 
384         internalNotifyViewAppeared((ViewStructureImpl) node);
385     }
386 
internalNotifyViewAppeared(@onNull ViewNode.ViewStructureImpl node)387     abstract void internalNotifyViewAppeared(@NonNull ViewNode.ViewStructureImpl node);
388 
389     /**
390      * Notifies the Content Capture Service that a node has been removed from the view structure.
391      *
392      * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
393      * automatically by the Android System for standard views.
394      *
395      * @param id id of the node that has been removed.
396      */
notifyViewDisappeared(@onNull AutofillId id)397     public final void notifyViewDisappeared(@NonNull AutofillId id) {
398         Objects.requireNonNull(id);
399         if (!isContentCaptureEnabled()) return;
400 
401         internalNotifyViewDisappeared(id);
402     }
403 
internalNotifyViewDisappeared(@onNull AutofillId id)404     abstract void internalNotifyViewDisappeared(@NonNull AutofillId id);
405 
406     /**
407      * Notifies the Content Capture Service that many nodes has been removed from a virtual view
408      * structure.
409      *
410      * <p>Should only be called by views that handle their own virtual view hierarchy.
411      *
412      * @param hostId id of the non-virtual view hosting the virtual view hierarchy (it can be
413      * obtained by calling {@link ViewStructure#getAutofillId()}).
414      * @param virtualIds ids of the virtual children.
415      *
416      * @throws IllegalArgumentException if the {@code hostId} is an autofill id for a virtual view.
417      * @throws IllegalArgumentException if {@code virtualIds} is empty
418      */
notifyViewsDisappeared(@onNull AutofillId hostId, @NonNull long[] virtualIds)419     public final void notifyViewsDisappeared(@NonNull AutofillId hostId,
420             @NonNull long[] virtualIds) {
421         Preconditions.checkArgument(hostId.isNonVirtual(), "hostId cannot be virtual: %s", hostId);
422         Preconditions.checkArgument(!ArrayUtils.isEmpty(virtualIds), "virtual ids cannot be empty");
423         if (!isContentCaptureEnabled()) return;
424 
425         // TODO(b/123036895): use a internalNotifyViewsDisappeared that optimizes how the event is
426         // parcelized
427         for (long id : virtualIds) {
428             internalNotifyViewDisappeared(new AutofillId(hostId, id, mId));
429         }
430     }
431 
432     /**
433      * Notifies the Intelligence Service that the value of a text node has been changed.
434      *
435      * @param id of the node.
436      * @param text new text.
437      */
notifyViewTextChanged(@onNull AutofillId id, @Nullable CharSequence text)438     public final void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
439         Objects.requireNonNull(id);
440 
441         if (!isContentCaptureEnabled()) return;
442 
443         internalNotifyViewTextChanged(id, text);
444     }
445 
internalNotifyViewTextChanged(@onNull AutofillId id, @Nullable CharSequence text)446     abstract void internalNotifyViewTextChanged(@NonNull AutofillId id,
447             @Nullable CharSequence text);
448 
449     /**
450      * Notifies the Intelligence Service that the insets of a view have changed.
451      */
notifyViewInsetsChanged(@onNull Insets viewInsets)452     public final void notifyViewInsetsChanged(@NonNull Insets viewInsets) {
453         Objects.requireNonNull(viewInsets);
454 
455         if (!isContentCaptureEnabled()) return;
456 
457         internalNotifyViewInsetsChanged(viewInsets);
458     }
459 
internalNotifyViewInsetsChanged(@onNull Insets viewInsets)460     abstract void internalNotifyViewInsetsChanged(@NonNull Insets viewInsets);
461 
462     /** @hide */
internalNotifyViewTreeEvent(boolean started)463     public abstract void internalNotifyViewTreeEvent(boolean started);
464 
465     /**
466      * Notifies the Content Capture Service that a session has resumed.
467      */
notifySessionResumed()468     public final void notifySessionResumed() {
469         if (!isContentCaptureEnabled()) return;
470 
471         internalNotifySessionResumed();
472     }
473 
internalNotifySessionResumed()474     abstract void internalNotifySessionResumed();
475 
476     /**
477      * Notifies the Content Capture Service that a session has paused.
478      */
notifySessionPaused()479     public final void notifySessionPaused() {
480         if (!isContentCaptureEnabled()) return;
481 
482         internalNotifySessionPaused();
483     }
484 
internalNotifySessionPaused()485     abstract void internalNotifySessionPaused();
486 
487     /**
488      * Creates a {@link ViewStructure} for a "standard" view.
489      *
490      * <p>This method should be called after a visible view is laid out; the view then must populate
491      * the structure and pass it to {@link #notifyViewAppeared(ViewStructure)}.
492      *
493      * <b>Note: </b>views that manage a virtual structure under this view must populate just the
494      * node representing this view and return right away, then asynchronously report (not
495      * necessarily in the UI thread) when the children nodes appear, disappear or have their text
496      * changed by calling {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)},
497      * {@link ContentCaptureSession#notifyViewDisappeared(AutofillId)}, and
498      * {@link ContentCaptureSession#notifyViewTextChanged(AutofillId, CharSequence)} respectively.
499      * The structure for the a child must be created using
500      * {@link ContentCaptureSession#newVirtualViewStructure(AutofillId, long)}, and the
501      * {@code autofillId} for a child can be obtained either through
502      * {@code childStructure.getAutofillId()} or
503      * {@link ContentCaptureSession#newAutofillId(AutofillId, long)}.
504      *
505      * <p>When the virtual view hierarchy represents a web page, you should also:
506      *
507      * <ul>
508      * <li>Call {@link ContentCaptureManager#getContentCaptureConditions()} to infer content capture
509      * events should be generate for that URL.
510      * <li>Create a new {@link ContentCaptureSession} child for every HTML element that renders a
511      * new URL (like an {@code IFRAME}) and use that session to notify events from that subtree.
512      * </ul>
513      *
514      * <p><b>Note: </b>the following methods of the {@code structure} will be ignored:
515      * <ul>
516      * <li>{@link ViewStructure#setChildCount(int)}
517      * <li>{@link ViewStructure#addChildCount(int)}
518      * <li>{@link ViewStructure#getChildCount()}
519      * <li>{@link ViewStructure#newChild(int)}
520      * <li>{@link ViewStructure#asyncNewChild(int)}
521      * <li>{@link ViewStructure#asyncCommit()}
522      * <li>{@link ViewStructure#setWebDomain(String)}
523      * <li>{@link ViewStructure#newHtmlInfoBuilder(String)}
524      * <li>{@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}
525      * <li>{@link ViewStructure#setDataIsSensitive(boolean)}
526      * <li>{@link ViewStructure#setAlpha(float)}
527      * <li>{@link ViewStructure#setElevation(float)}
528      * <li>{@link ViewStructure#setTransformation(android.graphics.Matrix)}
529      * </ul>
530      */
531     @NonNull
newViewStructure(@onNull View view)532     public final ViewStructure newViewStructure(@NonNull View view) {
533         return new ViewNode.ViewStructureImpl(view);
534     }
535 
536     /**
537      * Creates a new {@link AutofillId} for a virtual child, so it can be used to uniquely identify
538      * the children in the session.
539      *
540      * @param hostId id of the non-virtual view hosting the virtual view hierarchy (it can be
541      * obtained by calling {@link ViewStructure#getAutofillId()}).
542      * @param virtualChildId id of the virtual child, relative to the parent.
543      *
544      * @return if for the virtual child
545      *
546      * @throws IllegalArgumentException if the {@code parentId} is a virtual child id.
547      */
newAutofillId(@onNull AutofillId hostId, long virtualChildId)548     public @NonNull AutofillId newAutofillId(@NonNull AutofillId hostId, long virtualChildId) {
549         Objects.requireNonNull(hostId);
550         Preconditions.checkArgument(hostId.isNonVirtual(), "hostId cannot be virtual: %s", hostId);
551         return new AutofillId(hostId, virtualChildId, mId);
552     }
553 
554     /**
555      * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
556      * {@link #notifyViewAppeared(ViewStructure)} by the view managing the virtual view hierarchy.
557      *
558      * @param parentId id of the virtual view parent (it can be obtained by calling
559      * {@link ViewStructure#getAutofillId()} on the parent).
560      * @param virtualId id of the virtual child, relative to the parent.
561      *
562      * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
563      */
564     @NonNull
newVirtualViewStructure(@onNull AutofillId parentId, long virtualId)565     public final ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId,
566             long virtualId) {
567         return new ViewNode.ViewStructureImpl(parentId, virtualId, mId);
568     }
569 
isContentCaptureEnabled()570     boolean isContentCaptureEnabled() {
571         synchronized (mLock) {
572             return !mDestroyed;
573         }
574     }
575 
576     @CallSuper
dump(@onNull String prefix, @NonNull PrintWriter pw)577     void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
578         pw.print(prefix); pw.print("id: "); pw.println(mId);
579         if (mClientContext != null) {
580             pw.print(prefix); mClientContext.dump(pw); pw.println();
581         }
582         synchronized (mLock) {
583             pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
584             if (mChildren != null && !mChildren.isEmpty()) {
585                 final String prefix2 = prefix + "  ";
586                 final int numberChildren = mChildren.size();
587                 pw.print(prefix); pw.print("number children: "); pw.println(numberChildren);
588                 for (int i = 0; i < numberChildren; i++) {
589                     final ContentCaptureSession child = mChildren.get(i);
590                     pw.print(prefix); pw.print(i); pw.println(": "); child.dump(prefix2, pw);
591                 }
592             }
593         }
594     }
595 
596     @Override
toString()597     public String toString() {
598         return Integer.toString(mId);
599     }
600 
601     /** @hide */
602     @NonNull
getStateAsString(int state)603     protected static String getStateAsString(int state) {
604         return state + " (" + (state == UNKNOWN_STATE ? "UNKNOWN"
605                 : DebugUtils.flagsToString(ContentCaptureSession.class, "STATE_", state)) + ")";
606     }
607 
608     /** @hide */
609     @NonNull
getFlushReasonAsString(@lushReason int reason)610     public static String getFlushReasonAsString(@FlushReason int reason) {
611         switch (reason) {
612             case FLUSH_REASON_FULL:
613                 return "FULL";
614             case FLUSH_REASON_VIEW_ROOT_ENTERED:
615                 return "VIEW_ROOT";
616             case FLUSH_REASON_SESSION_STARTED:
617                 return "STARTED";
618             case FLUSH_REASON_SESSION_FINISHED:
619                 return "FINISHED";
620             case FLUSH_REASON_IDLE_TIMEOUT:
621                 return "IDLE";
622             case FLUSH_REASON_TEXT_CHANGE_TIMEOUT:
623                 return "TEXT_CHANGE";
624             case FLUSH_REASON_SESSION_CONNECTED:
625                 return "CONNECTED";
626             case FLUSH_REASON_FORCE_FLUSH:
627                 return "FORCE_FLUSH";
628             case FLUSH_REASON_VIEW_TREE_APPEARING:
629                 return "VIEW_TREE_APPEARING";
630             case FLUSH_REASON_VIEW_TREE_APPEARED:
631                 return "VIEW_TREE_APPEARED";
632             default:
633                 return "UNKOWN-" + reason;
634         }
635     }
636 
getRandomSessionId()637     private static int getRandomSessionId() {
638         int id;
639         do {
640             id = ID_GENERATOR.nextInt();
641         } while (id == NO_SESSION_ID);
642         return id;
643     }
644 }
645