Merge "Use PooledLambda in autofill code"

This commit is contained in:
Eugene Susla
2018-02-21 21:32:42 +00:00
committed by Android (Google) Code Review
5 changed files with 117 additions and 187 deletions

View File

@@ -15,12 +15,15 @@
*/
package android.service.autofill;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel;
@@ -30,9 +33,6 @@ import android.os.RemoteException;
import android.util.Log;
import android.view.autofill.AutofillValue;
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.SomeArgs;
import java.util.Arrays;
import java.util.List;
@@ -55,8 +55,6 @@ public abstract class AutofillFieldClassificationService extends Service {
private static final String TAG = "AutofillFieldClassificationService";
private static final int MSG_GET_SCORES = 1;
/**
* The {@link Intent} action that must be declared as handled by a service
* in its manifest for the system to recognize it as a quota providing service.
@@ -83,35 +81,18 @@ public abstract class AutofillFieldClassificationService extends Service {
private AutofillFieldClassificationServiceWrapper mWrapper;
private final HandlerCaller.Callback mHandlerCallback = (msg) -> {
final int action = msg.what;
private void getScores(RemoteCallback callback, String algorithmName, Bundle algorithmArgs,
List<AutofillValue> actualValues, String[] userDataValues) {
final Bundle data = new Bundle();
final RemoteCallback callback;
switch (action) {
case MSG_GET_SCORES:
final SomeArgs args = (SomeArgs) msg.obj;
callback = (RemoteCallback) args.arg1;
final String algorithmName = (String) args.arg2;
final Bundle algorithmArgs = (Bundle) args.arg3;
@SuppressWarnings("unchecked")
final List<AutofillValue> actualValues = ((List<AutofillValue>) args.arg4);
@SuppressWarnings("unchecked")
final String[] userDataValues = (String[]) args.arg5;
final float[][] scores = onGetScores(algorithmName, algorithmArgs, actualValues,
Arrays.asList(userDataValues));
if (scores != null) {
data.putParcelable(EXTRA_SCORES, new Scores(scores));
}
break;
default:
Log.w(TAG, "Handling unknown message: " + action);
return;
final float[][] scores = onGetScores(algorithmName, algorithmArgs, actualValues,
Arrays.asList(userDataValues));
if (scores != null) {
data.putParcelable(EXTRA_SCORES, new Scores(scores));
}
callback.sendResult(data);
};
}
private final HandlerCaller mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(),
mHandlerCallback, true);
private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
/** @hide */
public AutofillFieldClassificationService() {
@@ -160,9 +141,10 @@ public abstract class AutofillFieldClassificationService extends Service {
public void getScores(RemoteCallback callback, String algorithmName, Bundle algorithmArgs,
List<AutofillValue> actualValues, String[] userDataValues)
throws RemoteException {
// TODO(b/70939974): refactor to use PooledLambda
mHandlerCaller.obtainMessageOOOOO(MSG_GET_SCORES, callback, algorithmName,
algorithmArgs, actualValues, userDataValues).sendToTarget();
mHandler.sendMessage(obtainMessage(
AutofillFieldClassificationService::getScores,
AutofillFieldClassificationService.this,
callback, algorithmName, algorithmArgs, actualValues, userDataValues));
}
}

View File

@@ -15,6 +15,8 @@
*/
package android.service.autofill;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -22,6 +24,7 @@ import android.annotation.SdkConstant;
import android.app.Service;
import android.content.Intent;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.IBinder;
import android.os.ICancellationSignal;
import android.os.Looper;
@@ -34,9 +37,6 @@ import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.SomeArgs;
/**
* An {@code AutofillService} is a service used to automatically fill the contents of the screen
* on behalf of a given user - for more information about autofill, read
@@ -554,20 +554,12 @@ public abstract class AutofillService extends Service {
*/
public static final String SERVICE_META_DATA = "android.autofill";
// Handler messages.
private static final int MSG_CONNECT = 1;
private static final int MSG_DISCONNECT = 2;
private static final int MSG_ON_FILL_REQUEST = 3;
private static final int MSG_ON_SAVE_REQUEST = 4;
private final IAutoFillService mInterface = new IAutoFillService.Stub() {
@Override
public void onConnectedStateChanged(boolean connected) {
if (connected) {
mHandlerCaller.obtainMessage(MSG_CONNECT).sendToTarget();
} else {
mHandlerCaller.obtainMessage(MSG_DISCONNECT).sendToTarget();
}
mHandler.sendMessage(obtainMessage(
connected ? AutofillService::onConnected : AutofillService::onDisconnected,
AutofillService.this));
}
@Override
@@ -578,56 +570,27 @@ public abstract class AutofillService extends Service {
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
mHandlerCaller.obtainMessageOOO(MSG_ON_FILL_REQUEST, request,
CancellationSignal.fromTransport(transport), callback)
.sendToTarget();
mHandler.sendMessage(obtainMessage(
AutofillService::onFillRequest,
AutofillService.this, request, CancellationSignal.fromTransport(transport),
new FillCallback(callback, request.getId())));
}
@Override
public void onSaveRequest(SaveRequest request, ISaveCallback callback) {
mHandlerCaller.obtainMessageOO(MSG_ON_SAVE_REQUEST, request,
callback).sendToTarget();
mHandler.sendMessage(obtainMessage(
AutofillService::onSaveRequest,
AutofillService.this, request, new SaveCallback(callback)));
}
};
private final HandlerCaller.Callback mHandlerCallback = (msg) -> {
switch (msg.what) {
case MSG_CONNECT: {
onConnected();
break;
} case MSG_ON_FILL_REQUEST: {
final SomeArgs args = (SomeArgs) msg.obj;
final FillRequest request = (FillRequest) args.arg1;
final CancellationSignal cancellation = (CancellationSignal) args.arg2;
final IFillCallback callback = (IFillCallback) args.arg3;
final FillCallback fillCallback = new FillCallback(callback, request.getId());
args.recycle();
onFillRequest(request, cancellation, fillCallback);
break;
} case MSG_ON_SAVE_REQUEST: {
final SomeArgs args = (SomeArgs) msg.obj;
final SaveRequest request = (SaveRequest) args.arg1;
final ISaveCallback callback = (ISaveCallback) args.arg2;
final SaveCallback saveCallback = new SaveCallback(callback);
args.recycle();
onSaveRequest(request, saveCallback);
break;
} case MSG_DISCONNECT: {
onDisconnected();
break;
} default: {
Log.w(TAG, "MyCallbacks received invalid message type: " + msg);
}
}
};
private HandlerCaller mHandlerCaller;
private Handler mHandler;
@CallSuper
@Override
public void onCreate() {
super.onCreate();
mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(), mHandlerCallback, true);
mHandler = new Handler(Looper.getMainLooper(), null, true);
}
@Override

View File

@@ -41,6 +41,7 @@ import android.metrics.LogMaker;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteCallbackList;
@@ -76,7 +77,6 @@ import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.HandlerCaller;
import com.android.server.LocalServices;
import com.android.server.autofill.ui.AutoFillUI;
@@ -99,8 +99,6 @@ final class AutofillManagerServiceImpl {
/** Minimum interval to prune abandoned sessions */
private static final int MAX_ABANDONED_SESSION_MILLIS = 30000;
static final int MSG_SERVICE_SAVE = 1;
private final int mUserId;
private final Context mContext;
private final Object mLock;
@@ -151,18 +149,7 @@ final class AutofillManagerServiceImpl {
@GuardedBy("mLock")
private boolean mSetupComplete;
private final HandlerCaller.Callback mHandlerCallback = (msg) -> {
switch (msg.what) {
case MSG_SERVICE_SAVE:
handleSessionSave(msg.arg1);
break;
default:
Slog.w(TAG, "invalid msg on handler: " + msg);
}
};
private final HandlerCaller mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(),
mHandlerCallback, true);
private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
/**
* Cache of pending {@link Session}s, keyed by sessionId.
@@ -508,7 +495,7 @@ final class AutofillManagerServiceImpl {
assertCallerLocked(componentName);
final Session newSession = new Session(this, mUi, mContext, mHandlerCaller, mUserId, mLock,
final Session newSession = new Session(this, mUi, mContext, mHandler, mUserId, mLock,
sessionId, uid, activityToken, appCallbackToken, hasCallback, mUiLatencyHistory,
mWtfHistory, mInfo.getServiceInfo().getComponentName(), componentName, compatMode,
flags);
@@ -597,11 +584,10 @@ final class AutofillManagerServiceImpl {
mSessions.remove(sessionId);
}
private void handleSessionSave(int sessionId) {
void handleSessionSave(Session session) {
synchronized (mLock) {
final Session session = mSessions.get(sessionId);
if (session == null) {
Slog.w(TAG, "handleSessionSave(): already gone: " + sessionId);
if (mSessions.get(session.id) == null) {
Slog.w(TAG, "handleSessionSave(): already gone: " + session.id);
return;
}

View File

@@ -18,6 +18,7 @@ package com.android.server.autofill;
import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.autofill.Helper.sDebug;
import static com.android.server.autofill.Helper.sVerbose;
@@ -32,7 +33,6 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.ICancellationSignal;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -47,7 +47,6 @@ import android.text.format.DateUtils;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.HandlerCaller;
import com.android.server.FgThread;
import java.io.PrintWriter;
@@ -63,13 +62,14 @@ import java.lang.ref.WeakReference;
*/
final class RemoteFillService implements DeathRecipient {
private static final String LOG_TAG = "RemoteFillService";
// How long after the last interaction with the service we would unbind
private static final long TIMEOUT_IDLE_BIND_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS;
// How long after we make a remote request to a fill service we timeout
private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS;
private static final int MSG_UNBIND = 3;
private final Context mContext;
private final ComponentName mComponentName;
@@ -82,7 +82,7 @@ final class RemoteFillService implements DeathRecipient {
private final ServiceConnection mServiceConnection = new RemoteServiceConnection();
private final HandlerCaller mHandler;
private final Handler mHandler;
private IAutoFillService mAutoFillService;
@@ -115,14 +115,16 @@ final class RemoteFillService implements DeathRecipient {
mComponentName = componentName;
mIntent = new Intent(AutofillService.SERVICE_INTERFACE).setComponent(mComponentName);
mUserId = userId;
mHandler = new MyHandler(context);
mHandler = new Handler(FgThread.getHandler().getLooper());
}
public void destroy() {
mHandler.obtainMessage(MyHandler.MSG_DESTROY).sendToTarget();
mHandler.sendMessage(obtainMessage(
RemoteFillService::handleDestroy, this));
}
private void handleDestroy() {
if (checkIfDestroyed()) return;
if (mPendingRequest != null) {
mPendingRequest.cancel();
mPendingRequest = null;
@@ -133,10 +135,12 @@ final class RemoteFillService implements DeathRecipient {
@Override
public void binderDied() {
mHandler.obtainMessage(MyHandler.MSG_BINDER_DIED).sendToTarget();
mHandler.sendMessage(obtainMessage(
RemoteFillService::handleBinderDied, this));
}
private void handleBinderDied() {
if (checkIfDestroyed()) return;
if (mAutoFillService != null) {
mAutoFillService.asBinder().unlinkToDeath(this, 0);
}
@@ -174,14 +178,17 @@ final class RemoteFillService implements DeathRecipient {
public void onFillRequest(@NonNull FillRequest request) {
cancelScheduledUnbind();
final PendingFillRequest pendingRequest = new PendingFillRequest(request, this);
mHandler.obtainMessageO(MyHandler.MSG_ON_PENDING_REQUEST, pendingRequest).sendToTarget();
scheduleRequest(new PendingFillRequest(request, this));
}
public void onSaveRequest(@NonNull SaveRequest request) {
cancelScheduledUnbind();
final PendingSaveRequest pendingRequest = new PendingSaveRequest(request, this);
mHandler.obtainMessageO(MyHandler.MSG_ON_PENDING_REQUEST, pendingRequest).sendToTarget();
scheduleRequest(new PendingSaveRequest(request, this));
}
private void scheduleRequest(PendingRequest pendingRequest) {
mHandler.sendMessage(obtainMessage(
RemoteFillService::handlePendingRequest, this, pendingRequest));
}
// Note: we are dumping without a lock held so this is a bit racy but
@@ -204,21 +211,25 @@ final class RemoteFillService implements DeathRecipient {
}
private void cancelScheduledUnbind() {
mHandler.removeMessages(MyHandler.MSG_UNBIND);
mHandler.removeMessages(MSG_UNBIND);
}
private void scheduleUnbind() {
cancelScheduledUnbind();
Message message = mHandler.obtainMessage(MyHandler.MSG_UNBIND);
mHandler.sendMessageDelayed(message, TIMEOUT_IDLE_BIND_MILLIS);
mHandler.sendMessageDelayed(
obtainMessage(RemoteFillService::handleUnbind, this)
.setWhat(MSG_UNBIND),
TIMEOUT_IDLE_BIND_MILLIS);
}
private void handleUnbind() {
if (checkIfDestroyed()) return;
ensureUnbound();
}
private void handlePendingRequest(PendingRequest pendingRequest) {
if (mDestroyed || mCompleted) {
if (checkIfDestroyed()) return;
if (mCompleted) {
return;
}
if (!isBound()) {
@@ -283,7 +294,7 @@ final class RemoteFillService implements DeathRecipient {
private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest, int requestFlags,
FillResponse response) {
mHandler.getHandler().post(() -> {
mHandler.post(() -> {
if (handleResponseCallbackCommon(pendingRequest)) {
mCallbacks.onFillRequestSuccess(requestFlags, response,
mComponentName.getPackageName());
@@ -293,7 +304,7 @@ final class RemoteFillService implements DeathRecipient {
private void dispatchOnFillRequestFailure(PendingRequest pendingRequest,
@Nullable CharSequence message) {
mHandler.getHandler().post(() -> {
mHandler.post(() -> {
if (handleResponseCallbackCommon(pendingRequest)) {
mCallbacks.onFillRequestFailure(message, mComponentName.getPackageName());
}
@@ -301,7 +312,7 @@ final class RemoteFillService implements DeathRecipient {
}
private void dispatchOnFillTimeout(@NonNull ICancellationSignal cancellationSignal) {
mHandler.getHandler().post(() -> {
mHandler.post(() -> {
try {
cancellationSignal.cancel();
} catch (RemoteException e) {
@@ -312,7 +323,7 @@ final class RemoteFillService implements DeathRecipient {
private void dispatchOnSaveRequestSuccess(PendingRequest pendingRequest,
IntentSender intentSender) {
mHandler.getHandler().post(() -> {
mHandler.post(() -> {
if (handleResponseCallbackCommon(pendingRequest)) {
mCallbacks.onSaveRequestSuccess(mComponentName.getPackageName(), intentSender);
}
@@ -321,7 +332,7 @@ final class RemoteFillService implements DeathRecipient {
private void dispatchOnSaveRequestFailure(PendingRequest pendingRequest,
@Nullable CharSequence message) {
mHandler.getHandler().post(() -> {
mHandler.post(() -> {
if (handleResponseCallbackCommon(pendingRequest)) {
mCallbacks.onSaveRequestFailure(message, mComponentName.getPackageName());
}
@@ -378,44 +389,14 @@ final class RemoteFillService implements DeathRecipient {
}
}
private final class MyHandler extends HandlerCaller {
public static final int MSG_DESTROY = 1;
public static final int MSG_BINDER_DIED = 2;
public static final int MSG_UNBIND = 3;
public static final int MSG_ON_PENDING_REQUEST = 4;
public MyHandler(Context context) {
// Cannot use lambda - doesn't compile
super(context, FgThread.getHandler().getLooper(), new Callback() {
@Override
public void executeMessage(Message message) {
if (mDestroyed) {
if (sVerbose) {
Slog.v(LOG_TAG, "Not handling " + message + " as service for "
+ mComponentName + " is already destroyed");
}
return;
}
switch (message.what) {
case MSG_DESTROY: {
handleDestroy();
} break;
case MSG_BINDER_DIED: {
handleBinderDied();
} break;
case MSG_UNBIND: {
handleUnbind();
} break;
case MSG_ON_PENDING_REQUEST: {
handlePendingRequest((PendingRequest) message.obj);
} break;
}
}
}, false);
private boolean checkIfDestroyed() {
if (mDestroyed) {
if (sVerbose) {
Slog.v(LOG_TAG, "Not handling operation as service for "
+ mComponentName + " is already destroyed");
}
}
return mDestroyed;
}
private static abstract class PendingRequest implements Runnable {
@@ -433,7 +414,7 @@ final class RemoteFillService implements DeathRecipient {
PendingRequest(RemoteFillService service) {
mWeakService = new WeakReference<>(service);
mServiceHandler = service.mHandler.getHandler();
mServiceHandler = service.mHandler;
mTimeoutTrigger = () -> {
synchronized (mLock) {
if (mCancelled) {

View File

@@ -26,6 +26,7 @@ import static android.view.autofill.AutofillManager.ACTION_VALUE_CHANGED;
import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED;
import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.autofill.Helper.sDebug;
import static com.android.server.autofill.Helper.sPartitionMaxCount;
import static com.android.server.autofill.Helper.sVerbose;
@@ -49,6 +50,7 @@ import android.graphics.Rect;
import android.metrics.LogMaker;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Parcelable;
import android.os.RemoteCallback;
@@ -117,7 +119,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
private static final String EXTRA_REQUEST_ID = "android.service.autofill.extra.REQUEST_ID";
private final AutofillManagerServiceImpl mService;
private final HandlerCaller mHandlerCaller;
private final Handler mHandler;
private final Object mLock;
private final AutoFillUI mUi;
@@ -485,7 +487,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
Session(@NonNull AutofillManagerServiceImpl service, @NonNull AutoFillUI ui,
@NonNull Context context, @NonNull HandlerCaller handlerCaller, int userId,
@NonNull Context context, @NonNull Handler handler, int userId,
@NonNull Object lock, int sessionId, int uid, @NonNull IBinder activityToken,
@NonNull IBinder client, boolean hasCallback, @NonNull LocalLog uiLatencyHistory,
@NonNull LocalLog wtfHistory,
@@ -498,7 +500,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mService = service;
mLock = lock;
mUi = ui;
mHandlerCaller = handlerCaller;
mHandler = handler;
mRemoteFillService = new RemoteFillService(context, serviceComponentName, userId, this);
mActivityToken = activityToken;
mHasCallback = hasCallback;
@@ -726,8 +728,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mService.setAuthenticationSelected(id, mClientState);
final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex);
mHandlerCaller.getHandler().post(() -> startAuthentication(authenticationId,
intent, fillInIntent));
mHandler.sendMessage(obtainMessage(
Session::startAuthentication,
this, authenticationId, intent, fillInIntent));
}
// FillServiceCallbacks
@@ -746,7 +749,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return;
}
}
mHandlerCaller.getHandler().post(() -> autoFill(requestId, datasetIndex, dataset, true));
mHandler.sendMessage(obtainMessage(
Session::autoFill,
this, requestId, datasetIndex, dataset, true));
}
// AutoFillUiCallback
@@ -759,9 +764,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return;
}
}
mHandlerCaller.getHandler()
.obtainMessage(AutofillManagerServiceImpl.MSG_SERVICE_SAVE, id, 0)
.sendToTarget();
mHandler.sendMessage(obtainMessage(
AutofillManagerServiceImpl::handleSessionSave,
mService, this));
}
// AutoFillUiCallback
@@ -776,7 +781,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return;
}
}
mHandlerCaller.getHandler().post(() -> removeSelf());
mHandler.sendMessage(obtainMessage(
Session::removeSelf, this));
}
// AutoFillUiCallback
@@ -831,15 +837,19 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
removeSelfLocked();
}
mHandlerCaller.getHandler().post(() -> {
try {
synchronized (mLock) {
mClient.startIntentSender(intentSender, null);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error launching auth intent", e);
mHandler.sendMessage(obtainMessage(
Session::doStartIntentSender,
this, intentSender));
}
private void doStartIntentSender(IntentSender intentSender) {
try {
synchronized (mLock) {
mClient.startIntentSender(intentSender, null);
}
});
} catch (RemoteException e) {
Slog.e(TAG, "Error launching auth intent", e);
}
}
@GuardedBy("mLock")
@@ -960,11 +970,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
* when necessary.
*/
public void logContextCommitted() {
mHandlerCaller.getHandler().post(() -> {
synchronized (mLock) {
logContextCommittedLocked();
}
});
mHandler.sendMessage(obtainMessage(
Session::doLogContextCommitted, this));
}
private void doLogContextCommitted() {
synchronized (mLock) {
logContextCommittedLocked();
}
}
@GuardedBy("mLock")
@@ -1486,7 +1499,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
// Use handler so logContextCommitted() is logged first
mHandlerCaller.getHandler().post(() -> mService.logSaveShown(id, mClientState));
mHandler.sendMessage(obtainMessage(
Session::logSaveShown, this));
final IAutoFillManagerClient client = getClient();
mPendingSaveUi = new PendingUi(mActivityToken, id, client);
@@ -1514,6 +1528,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return true;
}
private void logSaveShown() {
mService.logSaveShown(id, mClientState);
}
@Nullable
private ArrayMap<AutofillId, InternalSanitizer> createSanitizers(@Nullable SaveInfo saveInfo) {
if (saveInfo == null) return null;