public final class Continuity
- Object
- Continuity
Saves what the user was doing, brings it back when the app starts again, and – where the platform or your own endpoint can carry it – lets them pick it up on another device.
// in init()
Continuity.setStateProvider(new StateProvider() {
public Map<String, Object> saveState() {
Map<String, Object> m = new HashMap<String, Object>();
m.put("draft", draftField.getText());
return m;
}
public void restoreState(Map<String, Object> payload) {
pendingDraft = (String) payload.get("draft");
}
});
// in start()
if (!Continuity.restore()) {
Navigation.navigate("/home");
}
What is saved
Two halves. The framework contributes the com.codename1.router.Navigation stack, so an app
whose screens are declared with @Route gets them back with no code at all. Your
StateProvider contributes everything else. An app that navigates with new MyForm().show()
has no route stack to save – those navigations are not addressable – and restores from the
payload alone.
When it is saved
Continuously, not at shutdown. Every navigation marks the state dirty and a checkpoint is
written once per event loop pass, so by the time the operating system suspends the app the work
is already done. This is deliberate: on Android the platform blocks its own main thread until
the app’s stop() returns, and an app that did its saving there would be paying for it on
every single suspend. Call checkpoint() directly after changing something the provider
reports but no navigation touched.
Getting it back
restore() returns true when it showed something, so start() reads as “restore, or else
begin”. It is never called for you: an app that adopts this API decides where restoration fits
in its own launch, and an app that does not is completely unaffected.
Other devices
isContinuationSupported() reports whether this platform can advertise the current state to
the user’s nearby devices; on Apple platforms it can, elsewhere it cannot and the call is a
no-op rather than an error. For everything the platform will not carry – iOS to Android, two
devices that are never together – set a StateRelay, which is your own endpoint. Codename One
runs no server for this, because deciding which states belong to the same person is your
account system’s job.
Arriving states are offered to every ContinuityListener before anything happens, and this
device’s own echo is never offered at all.
Zero cost when unused
Referencing this package is what makes the build declare the activity type on Apple platforms
and compile the native continuation handling in. An app that never touches
com.codename1.continuity gets none of it.
Threading
Call this class from the event dispatch thread, like the rest of the toolkit. Codename One is single threaded: every method here runs on the EDT and every field it keeps is owned by the EDT, so there is nothing to synchronize and nothing that can interleave.
Two kinds of foreign thread exist, and both hand over at the boundary rather than reaching in.
A port delivering a continuation arrives on the platform’s own thread and is marshalled with
com.codename1.ui.Display#callSerially. The relay’s publish and fetch are blocking calls
that must not sit on the EDT, so they run on a worker – one that is handed the state it needs
as a parameter, touches no field of this class, and returns its answer through callSerially
as well. That is the whole concurrency design, and it is deliberately the toolkit’s: one
thread on each side of a boundary, never two on the same state.
Methods
public static void enable() | Turns the framework on. |
public static void disable() | Turns the framework off. |
public static boolean isEnabled() | Whether the framework is on. |
public static boolean isSupported() | Whether this platform can save and restore state at all. |
public static boolean isContinuationSupported() | Whether this platform can advertise the current state to the user’s other devices while they are together. |
public static void setStateProvider(StateProvider p) | Installs the object that supplies and consumes the application half of the state, and enables the framework. |
public static StateProvider getStateProvider() | The installed state provider, or null. |
public static void addContinuationListener(ContinuityListener l) | Registers a listener for states arriving from elsewhere. |
public static void removeContinuationListener(ContinuityListener l) | Removes a listener. |
public static void setRelay(StateRelay r) | Installs the endpoint that carries state to devices the platform will not reach, and asks it immediately for anything newer than what is here. |
public static StateRelay getRelay() | The installed relay, or null. |
public static void setAutoRestore(boolean b) | Whether a restorable state found at startup, or arriving from another device, is applied automatically. |
public static boolean isAutoRestore() | Whether automatic restoration is on. |
public static void setTitle(String t) | Sets the label a receiving device may show before the user accepts a continuation – “Draft to Dana”, “Invoice 2031”. |
public static String getTitle() | The current continuation label, or null. |
public static void setMaxAge(long millis) | How old a stored state may be and still be restored, in milliseconds. |
public static long getMaxAge() | The staleness limit in milliseconds, or 0 for none. |
public static String getDeviceId() | This installation’s device id, the value that lets a state be recognized as this device’s own echo when it comes back through a relay. |
public static boolean restoreSessionEnded() | Internal. |
public static void routeStackChanged() | Internal. |
public static void checkpoint() | Writes the current state now, and offers it to every enabled channel: storage always, the platform’s continuation where there is one, and the relay if one is set. |
public static boolean isCheckpointPending() | Internal. |
public static AppState capture() | Builds a state from the route stack and the provider. |
public static AppState getRestorableState() | The state waiting to be restored: one that arrived from another device if there is one, otherwise the last checkpoint written on this device. |
public static boolean restore() | Restores whatever getRestorableState() offers. |
public static void acknowledge(AppState state) | Records that the application has handled state itself, so it is not offered again. |
public static boolean restore(AppState state) | Restores a specific state: hands its payload to the provider, then replays its route stack. |
public static void pollRelay() | Asks the relay for anything newer than what is here, on a background thread. |
public static void clear() | Forgets everything: the stored checkpoint, any parked arrival, the activity advertised to the user’s other devices, and anything queued for the relay. |
public static String getActivityType() | The activity type this app publishes and answers to, which is the app’s package name followed by .continuity. |
public static void setBridge(ContinuityBridge b) | Test seam: installs a bridge, bypassing platform resolution. |
public static ContinuityBridge bridgeForSyncedStore() | Internal. |
public static void installSyncedStoreCallback() | Internal. |
public static void refreshBridge() | Internal. |
Inherited methods
Method details
enable
public static void enable()Turns the framework on. Called for you by setStateProvider(StateProvider); call it
directly when the route stack alone is all you need saved.
Nothing before this call has any effect, which is what keeps an app that does not use this API behaving exactly as it always did.
disable
public static void disable()clear() to
remove it.isEnabled
public static boolean isEnabled()Returns
isSupported
public static boolean isSupported()Display has been initialized.Returns
isContinuationSupported
public static boolean isContinuationSupported()Whether this platform can advertise the current state to the user’s other devices while they are together.
Branch on this rather than on the platform name: it is true on Apple platforms today and
the set is expected to grow, and a com.codename1.ui.Display#getPlatformName test would
have to be found and changed when it does.
Returns
setStateProvider
public static void setStateProvider(StateProvider p)Parameters
pStateProvider- the provider, or null to contribute nothing beyond the route stack
getStateProvider
public static StateProvider getStateProvider()Returns
addContinuationListener
public static void addContinuationListener(ContinuityListener l)Parameters
lContinuityListener- the listener
removeContinuationListener
public static void removeContinuationListener(ContinuityListener l)Parameters
lContinuityListener- the listener
setRelay
public static void setRelay(StateRelay r)Parameters
rStateRelay- the relay, or null to stop using one
getRelay
public static StateRelay getRelay()Returns
setAutoRestore
public static void setAutoRestore(boolean b)Whether a restorable state found at startup, or arriving from another device, is applied automatically. On by default.
Turning it off leaves restore() and every listener working exactly as before; what stops
is the framework acting on its own. Use it when the decision to move the user is always
the app’s.
Parameters
bboolean- true to restore automatically
isAutoRestore
public static boolean isAutoRestore()Returns
setTitle
public static void setTitle(String t)Parameters
tString- the label, or null for none
Throws
java.lang.IllegalArgumentException- when the label is longer than a stored checkpoint can hold
getTitle
public static String getTitle()Returns
setMaxAge
public static void setMaxAge(long millis)How old a stored state may be and still be restored, in milliseconds. Zero, the default, means no limit: an app the user opens after a month comes back where they left it, which is what they expect of it.
Set it when coming back is only meaningful for a while – a checkout, a queue position, a booking hold.
Parameters
millislong- the limit, or 0 for none
getMaxAge
public static long getMaxAge()Returns
getDeviceId
public static String getDeviceId()Returns
restoreSessionEnded
public static boolean restoreSessionEnded()Internal. Called by com.codename1.router.Navigation between the route factories of a
restore, and again before it shows the rebuilt screen. True once a factory has ended the
session – so the rebuild stops instead of running every remaining factory against an
account that has just signed out.
The lifecycle check in restore() runs only after restoreStack() has returned, which is
far too late for this: by then those factories have constructed their forms, and whatever
they queried or wrote for the signed-out account is done. Emptying the stack afterwards
undoes none of it.
A direct call for the reason routeStackChanged() gives: it answers false immediately
unless a restore is actually in progress, and a listener registry here would be public API
earned by one internal caller.
Returns
routeStackChanged
public static void routeStackChanged()com.codename1.router.Navigation after every change to the navigation
stack; schedules a checkpoint rather than taking one, so a burst of navigations costs a
single write.checkpoint
public static void checkpoint()Writes the current state now, and offers it to every enabled channel: storage always, the platform’s continuation where there is one, and the relay if one is set.
Cheap enough to call freely – the state is a list of paths and a small map – but it does touch storage, so it belongs at the end of a change rather than inside a loop.
Throws
IllegalArgumentException- when the provider returned a payload that cannot cross to another device
isCheckpointPending
public static boolean isCheckpointPending()Internal. Whether a checkpoint is owed – something changed since the last one was written.
Exists so a port with a suspend callback can skip the event-thread round trip entirely in the common case, where the write-through already happened as the user navigated.
Returns
checkpoint() would write something newcapture
public static AppState capture()Builds a state from the route stack and the provider. Useful for sending one somewhere of your own.
The state itself is not stored – only checkpoint() does that – but the sequence counter
it allocates is remembered, so states keep a rising order across a relaunch even for an
application that never checkpoints. When that counter cannot be written this returns null,
because a state carrying a number this device will hand out again is unsafe to send.
Returns
Throws
IllegalArgumentException- when the provider returned an unrepresentable payload
getRestorableState
public static AppState getRestorableState()Returns
getMaxAge()restore
public static boolean restore()Restores whatever getRestorableState() offers.
Written to read as “restore, or else begin”:
public void start() {
if (!Continuity.restore()) {
Navigation.navigate("/home");
}
}
Returns
acknowledge
public static void acknowledge(AppState state)Records that the application has handled state itself, so it is not offered again.
For the pattern ContinuityListener documents: do the work yourself and return false. That
path never reaches restore(), so nothing recorded the acknowledgement durably – the
sequence stayed in this process only, and after a relaunch the relay’s unchanged document
was accepted again and the listener repeated its side effects, against the act-once
guarantee.
Deliberately NOT inferred from a false return. False also means “keep it, I will prompt and call restore() when the user accepts”, and marking that handled immediately would lose the state if the process died before they answered – which is the same data loss as marking a parked state. The two intentions are different, so the application says which it means.
Parameters
stateAppState- the state that has been dealt with
restore
public static boolean restore(AppState state)Restores a specific state: hands its payload to the provider, then replays its route stack.
This is the second half of the “ask first” pattern – a ContinuityListener that returned
false to hold a state calls this once the user accepts it.
Parameters
stateAppState- the state, or null
Returns
pollRelay
public static void pollRelay()Asks the relay for anything newer than what is here, on a background thread. Returns immediately.
Worth calling when the app comes back to the foreground: a continuation reaches a nearby device on its own, but a relay is only read when something asks it to be.
clear
public static void clear()Forgets everything: the stored checkpoint, any parked arrival, the activity advertised to the user’s other devices, and anything queued for the relay.
Belongs on your logout path, PAIRED WITH disable(), and enable() on the way back in.
This call alone is not a logout. It forgets the account’s data and deliberately leaves continuity switched ON, because forgetting state and turning the feature off are two different things: an application is entitled to do the first without the second – a “start over” that is not a sign-out – and making this imply the other would stop continuity dead for every app that used it that way and never called enable() again.
The consequence, if it is not paired: a continuation that arrives while the login screen is up reaches a framework that is still listening and still enabled. It is a valid arrival by every test this class makes – it came AFTER the clear, so it is not from the session that ended – so the signed-out account’s routes and payload are restored over the login screen and written to storage. disable() is what closes that gap; enable() at login reopens it.
The advertised activity outlives the app’s own screen, so an account’s work would otherwise stay offered to the devices around it after the user signed out – and a queued relay publish would have gone out later under whatever credentials the relay returned by then, which after a logout is the NEXT account’s.
One thing it cannot undo: a relay request already on the wire when this is called. Nothing in this process can recall that. What this guarantees is that nothing follows it.
getActivityType
public static String getActivityType()The activity type this app publishes and answers to, which is the app’s package name
followed by .continuity.
Fixed by the build, which declares the same string to the platform in NSUserActivityTypes;
the two have to agree or the operating system refuses to deliver anything. Exposed because
an app that also publishes activities of its own needs to know which one is this
framework’s, and because it is the first thing to check when a continuation never arrives.
Returns
setBridge
public static void setBridge(ContinuityBridge b)Parameters
bContinuityBridge- the bridge, or null to resolve from the platform again
bridgeForSyncedStore
public static ContinuityBridge bridgeForSyncedStore()com.codename1.continuity.sync, which is a
package of its own so that its entitlement is earned separately. Application code uses
com.codename1.continuity.sync.SyncedStore.Returns
installSyncedStoreCallback
public static void installSyncedStoreCallback()Internal. Installs the inbound seam WITHOUT turning continuity on. Application code uses
com.codename1.continuity.sync.SyncedStore.addChangeListener.
com.codename1.continuity.sync is a package of its own precisely so that its cost is
earned separately, and enable() is not a cost the synced store asks for: it makes every
route change checkpoint, and a checkpoint advertises the app’s navigation to the devices
around it over Handoff. Registering a store listener used to call it, so an application
that wanted a key/value store the user’s devices share – and nothing else – was opted
into broadcasting its route stack.
The store’s own notification does not go through enabled (see Callback.syncedStoreChanged),
which is what lets the listener work with continuity still off.
refreshBridge
public static void refreshBridge()