blob: c19910f0e164074f604311b5c44346fb023a1885 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.autofillservice.cts.testcore;
import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.FAILURE;
import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.NULL;
import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.TIMEOUT;
import static android.autofillservice.cts.testcore.Helper.dumpStructure;
import static android.autofillservice.cts.testcore.Helper.getActivityName;
import static android.autofillservice.cts.testcore.Timeouts.CONNECTION_TIMEOUT;
import static android.autofillservice.cts.testcore.Timeouts.FILL_EVENTS_TIMEOUT;
import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
import static android.autofillservice.cts.testcore.Timeouts.IDLE_UNBIND_TIMEOUT;
import static android.autofillservice.cts.testcore.Timeouts.RESPONSE_DELAY_MS;
import static android.autofillservice.cts.testcore.Timeouts.SAVE_TIMEOUT;
import static com.google.common.truth.Truth.assertThat;
import android.app.assist.AssistStructure;
import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
import android.autofillservice.cts.testcore.CannedFillResponse.ResponseType;
import android.content.ComponentName;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
import android.service.autofill.FillCallback;
import android.service.autofill.FillContext;
import android.service.autofill.FillEventHistory;
import android.service.autofill.FillEventHistory.Event;
import android.service.autofill.FillResponse;
import android.service.autofill.SaveCallback;
import android.service.autofill.SavedDatasetsInfoCallback;
import android.util.Log;
import android.view.inputmethod.InlineSuggestionsRequest;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.compatibility.common.util.RetryableException;
import com.android.compatibility.common.util.TestNameUtils;
import com.android.compatibility.common.util.Timeout;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* Implementation of {@link AutofillService} used in the tests.
*/
public class InstrumentedAutoFillService extends AutofillService {
public static final String SERVICE_PACKAGE = Helper.MY_PACKAGE;
public static final String SERVICE_CLASS = "InstrumentedAutoFillService";
public static final String SERVICE_NAME = SERVICE_PACKAGE + "/.testcore." + SERVICE_CLASS;
public static String sServiceLabel = SERVICE_CLASS;
// TODO(b/125844305): remove once fixed
private static final boolean FAIL_ON_INVALID_CONNECTION_STATE = false;
private static final String TAG = "InstrumentedAutoFillService";
private static final boolean DUMP_FILL_REQUESTS = false;
private static final boolean DUMP_SAVE_REQUESTS = false;
protected static final AtomicReference<InstrumentedAutoFillService> sInstance =
new AtomicReference<>();
private static final Replier sReplier = new Replier();
@Nullable
private static Consumer<SavedDatasetsInfoCallback> sSavedDatasetsInfoReplier;
private static AtomicBoolean sConnected = new AtomicBoolean(false);
// We must handle all requests in a separate thread as the service's main thread is the also
// the UI thread of the test process and we don't want to hose it in case of failures here
private static final HandlerThread sMyThread = new HandlerThread("MyServiceThread");
private final Handler mHandler;
private boolean mConnected;
static {
Log.i(TAG, "Starting thread " + sMyThread);
sMyThread.start();
}
public InstrumentedAutoFillService() {
sInstance.set(this);
sServiceLabel = SERVICE_CLASS;
mHandler = Handler.createAsync(sMyThread.getLooper());
sReplier.setHandler(mHandler);
}
private static InstrumentedAutoFillService peekInstance() {
return sInstance.get();
}
/**
* Gets the list of fill events in the {@link FillEventHistory}, waiting until it has the
* expected size.
*/
public static List<Event> getFillEvents(int expectedSize) throws Exception {
final List<Event> events = getFillEventHistory(expectedSize).getEvents();
// Validation check
if (expectedSize > 0 && events == null || events.size() != expectedSize) {
throw new IllegalStateException("INTERNAL ERROR: events should have " + expectedSize
+ ", but it is: " + events);
}
return events;
}
/**
* Gets the {@link FillEventHistory}, waiting until it has the expected size.
*/
public static FillEventHistory getFillEventHistory(int expectedSize) throws Exception {
final InstrumentedAutoFillService service = peekInstance();
if (expectedSize == 0) {
// Need to always sleep as there is no condition / callback to be used to wait until
// expected number of events is set.
SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
final FillEventHistory history = service.getFillEventHistory();
assertThat(history.getEvents()).isNull();
return history;
}
return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> {
final FillEventHistory history = service.getFillEventHistory();
if (history == null) {
return null;
}
final List<Event> events = history.getEvents();
if (events != null) {
if (events.size() != expectedSize) {
Log.v(TAG, "Didn't get " + expectedSize + " events yet: " + events);
return null;
}
} else {
Log.v(TAG, "Events is still null (expecting " + expectedSize + ")");
return null;
}
return history;
});
}
/**
* Asserts there is no {@link FillEventHistory}.
*/
public static void assertNoFillEventHistory() {
// Need to always sleep as there is no condition / callback to be used to wait until
// expected number of events is set.
SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
assertThat(peekInstance().getFillEventHistory()).isNull();
}
/**
* Gets the service label associated with the current instance.
*/
public static String getServiceLabel() {
return sServiceLabel;
}
private void handleConnected(boolean connected) {
Log.v(TAG, "handleConnected(): from " + sConnected.get() + " to " + connected);
sConnected.set(connected);
}
@Override
public void onConnected() {
Log.v(TAG, "onConnected");
if (mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
dumpSelf();
sReplier.addException(new IllegalStateException("onConnected() called again"));
}
mConnected = true;
mHandler.post(() -> handleConnected(true));
}
@Override
public void onDisconnected() {
Log.v(TAG, "onDisconnected");
if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
dumpSelf();
sReplier.addException(
new IllegalStateException("onDisconnected() called when disconnected"));
}
mConnected = false;
mHandler.post(() -> handleConnected(false));
}
@Override
public void onFillRequest(android.service.autofill.FillRequest request,
CancellationSignal cancellationSignal, FillCallback callback) {
final ComponentName component = getLastActivityComponent(request.getFillContexts());
if (DUMP_FILL_REQUESTS) {
dumpStructure("onFillRequest()", request.getFillContexts());
} else {
Log.i(TAG, "onFillRequest() for " + component.toShortString());
}
if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
dumpSelf();
sReplier.addException(
new IllegalStateException("onFillRequest() called when disconnected"));
}
if (!TestNameUtils.isRunningTest()) {
Log.e(TAG, "onFillRequest(" + component + ") called after tests finished");
return;
}
if (!fromSamePackage(component)) {
Log.w(TAG, "Ignoring onFillRequest() from different package: " + component);
return;
}
mHandler.post(
() -> sReplier.onFillRequest(request.getFillContexts(), request.getClientState(),
cancellationSignal, callback, request.getFlags(),
request.getInlineSuggestionsRequest(),
request.getDelayedFillIntentSender(),
request.getId()));
}
@Override
public void onSaveRequest(android.service.autofill.SaveRequest request,
SaveCallback callback) {
if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
dumpSelf();
sReplier.addException(
new IllegalStateException("onSaveRequest() called when disconnected"));
}
mHandler.post(()->handleSaveRequest(request, callback));
}
private void handleSaveRequest(android.service.autofill.SaveRequest request,
SaveCallback callback) {
final ComponentName component = getLastActivityComponent(request.getFillContexts());
if (!TestNameUtils.isRunningTest()) {
Log.e(TAG, "onSaveRequest(" + component + ") called after tests finished");
return;
}
if (!fromSamePackage(component)) {
Log.w(TAG, "Ignoring onSaveRequest() from different package: " + component);
return;
}
if (DUMP_SAVE_REQUESTS) {
dumpStructure("onSaveRequest()", request.getFillContexts());
} else {
Log.i(TAG, "onSaveRequest() for " + component.toShortString());
}
mHandler.post(() -> sReplier.onSaveRequest(request.getFillContexts(),
request.getClientState(), callback,
request.getDatasetIds()));
}
@Override
public void onSavedDatasetsInfoRequest(@NonNull SavedDatasetsInfoCallback callback) {
if (sSavedDatasetsInfoReplier == null) {
super.onSavedDatasetsInfoRequest(callback);
} else {
sSavedDatasetsInfoReplier.accept(callback);
}
}
public static void setSavedDatasetsInfoReplier(
@Nullable Consumer<SavedDatasetsInfoCallback> savedDatasetsInfoReplier) {
sSavedDatasetsInfoReplier = savedDatasetsInfoReplier;
}
public static boolean isConnected() {
return sConnected.get();
}
private boolean fromSamePackage(ComponentName component) {
final String actualPackage = component.getPackageName();
if (!actualPackage.equals(getPackageName())
&& !actualPackage.equals(sReplier.mAcceptedPackageName)) {
Log.w(TAG, "Got request from package " + actualPackage);
return false;
}
return true;
}
private ComponentName getLastActivityComponent(List<FillContext> contexts) {
return contexts.get(contexts.size() - 1).getStructure().getActivityComponent();
}
private void dumpSelf() {
try {
try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
dump(null, pw, null);
pw.flush();
final String dump = sw.toString();
Log.e(TAG, "dumpSelf(): " + dump);
}
} catch (IOException e) {
Log.e(TAG, "I don't always fail to dump, but when I do, I dump the failure", e);
}
}
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.print("sConnected: "); pw.println(sConnected);
pw.print("mConnected: "); pw.println(mConnected);
pw.print("sInstance: "); pw.println(sInstance);
pw.println("sReplier: "); sReplier.dump(pw);
}
/**
* Waits until {@link #onConnected()} is called, or fails if it times out.
*
* <p>This method is useful on tests that explicitly verifies the connection, but should be
* avoided in other tests, as it adds extra time to the test execution (and flakiness in cases
* where the service might have being disconnected already; for example, if the fill request
* was replied with a {@code null} response) - if a text needs to block until the service
* receives a callback, it should use {@link Replier#getNextFillRequest()} instead.
*/
public static void waitUntilConnected() throws Exception {
waitConnectionState(CONNECTION_TIMEOUT, true);
}
/**
* Waits until {@link #onDisconnected()} is called, or fails if it times out.
*
* <p>This method is useful on tests that explicitly verifies the connection, but should be
* avoided in other tests, as it adds extra time to the test execution.
*/
public static void waitUntilDisconnected() throws Exception {
waitConnectionState(IDLE_UNBIND_TIMEOUT, false);
}
private static void waitConnectionState(Timeout timeout, boolean expected) throws Exception {
timeout.run("wait for connected=" + expected, () -> {
return isConnected() == expected ? Boolean.TRUE : null;
});
}
/**
* Gets the {@link Replier} singleton.
*/
public static Replier getReplier() {
return sReplier;
}
public static void resetStaticState() {
sInstance.set(null);
sConnected.set(false);
sServiceLabel = SERVICE_CLASS;
sSavedDatasetsInfoReplier = null;
}
/**
* POJO representation of the contents of a
* {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
* CancellationSignal, FillCallback)} that can be asserted at the end of a test case.
*/
public static final class FillRequest {
public final AssistStructure structure;
public final List<FillContext> contexts;
public final Bundle data;
public final CancellationSignal cancellationSignal;
public final FillCallback callback;
public final int flags;
public final InlineSuggestionsRequest inlineRequest;
public final IntentSender delayFillIntentSender;
public final int requestId;
private FillRequest(List<FillContext> contexts, Bundle data,
CancellationSignal cancellationSignal, FillCallback callback, int flags,
InlineSuggestionsRequest inlineRequest,
IntentSender delayFillIntentSender,
int requestId) {
this.contexts = contexts;
this.data = data;
this.cancellationSignal = cancellationSignal;
this.callback = callback;
this.flags = flags;
this.structure = contexts.get(contexts.size() - 1).getStructure();
this.inlineRequest = inlineRequest;
this.delayFillIntentSender = delayFillIntentSender;
this.requestId = requestId;
}
@Override
public String toString() {
return "FillRequest[activity=" + getActivityName(contexts) + ", flags=" + flags
+ ", bundle=" + data + ", structure=" + Helper.toString(structure) + "]";
}
}
/**
* POJO representation of the contents of a
* {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)}
* that can be asserted at the end of a test case.
*/
public static final class SaveRequest {
public final List<FillContext> contexts;
public final AssistStructure structure;
public final Bundle data;
public final SaveCallback callback;
public final List<String> datasetIds;
private SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
List<String> datasetIds) {
if (contexts != null && contexts.size() > 0) {
structure = contexts.get(contexts.size() - 1).getStructure();
} else {
structure = null;
}
this.contexts = contexts;
this.data = data;
this.callback = callback;
this.datasetIds = datasetIds;
}
@Override
public String toString() {
return "SaveRequest:" + getActivityName(contexts);
}
}
/**
* Object used to answer a
* {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
* CancellationSignal, FillCallback)}
* on behalf of a unit test method.
*/
public static final class Replier {
private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>();
private final BlockingQueue<FillRequest> mFillRequests = new LinkedBlockingQueue<>();
private final BlockingQueue<SaveRequest> mSaveRequests = new LinkedBlockingQueue<>();
private List<Throwable> mExceptions;
private IntentSender mOnSaveIntentSender;
private String mAcceptedPackageName;
private Handler mHandler;
private boolean mReportUnhandledFillRequest = true;
private boolean mReportUnhandledSaveRequest = true;
private Replier() {
}
private IdMode mIdMode = IdMode.RESOURCE_ID;
public void setIdMode(IdMode mode) {
this.mIdMode = mode;
}
public void acceptRequestsFromPackage(String packageName) {
mAcceptedPackageName = packageName;
}
/**
* Gets the exceptions thrown asynchronously, if any.
*/
@Nullable
public List<Throwable> getExceptions() {
return mExceptions;
}
private void addException(@Nullable Throwable e) {
if (e == null) return;
if (mExceptions == null) {
mExceptions = new ArrayList<>();
}
mExceptions.add(e);
}
/**
* Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with just
* one {@link Dataset}.
*/
public Replier addResponse(CannedDataset dataset) {
return addResponse(new CannedFillResponse.Builder()
.addDataset(dataset)
.build());
}
/**
* Sets the expectation for the next {@code onFillRequest}.
*/
public Replier addResponse(CannedFillResponse response) {
if (response == null) {
throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
}
mResponses.add(response);
return this;
}
/**
* Sets the {@link IntentSender} that is passed to
* {@link SaveCallback#onSuccess(IntentSender)}.
*/
public Replier setOnSave(IntentSender intentSender) {
mOnSaveIntentSender = intentSender;
return this;
}
/**
* Gets the next fill request, in the order received.
*/
public FillRequest getNextFillRequest() {
FillRequest request;
try {
request = mFillRequests.poll(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted", e);
}
if (request == null) {
throw new RetryableException(FILL_TIMEOUT, "onFillRequest() not called");
}
return request;
}
/**
* Asserts that {@link #onFillRequest(List, Bundle, CancellationSignal, FillCallback, int)}
* was not called.
*
* <p>Should only be called in cases where it's not expected to be called, as it will
* sleep for a few ms.
*/
public void assertOnFillRequestNotCalled() {
SystemClock.sleep(FILL_TIMEOUT.getMaxValue());
assertThat(mFillRequests).isEmpty();
}
/**
* Asserts all {@link AutofillService#onFillRequest(
* android.service.autofill.FillRequest, CancellationSignal, FillCallback) fill requests}
* received by the service were properly {@link #getNextFillRequest() handled} by the test
* case.
*/
public void assertNoUnhandledFillRequests() {
if (mFillRequests.isEmpty()) return; // Good job, test case!
if (!mReportUnhandledFillRequest) {
// Just log, so it's not thrown again on @After if already thrown on main body
Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
+ "but logging just in case: " + mFillRequests);
return;
}
mReportUnhandledFillRequest = false;
throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
+ mFillRequests);
}
/**
* Gets the current number of unhandled requests.
*/
public int getNumberUnhandledFillRequests() {
return mFillRequests.size();
}
/**
* Gets the next save request, in the order received.
*
* <p>Typically called at the end of a test case, to assert the initial request.
*/
public SaveRequest getNextSaveRequest() {
SaveRequest request;
try {
request = mSaveRequests.poll(SAVE_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted", e);
}
if (request == null) {
throw new RetryableException(SAVE_TIMEOUT, "onSaveRequest() not called");
}
return request;
}
/**
* Asserts all
* {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)
* save requests} received by the service were properly
* {@link #getNextFillRequest() handled} by the test case.
*/
public void assertNoUnhandledSaveRequests() {
if (mSaveRequests.isEmpty()) return; // Good job, test case!
if (!mReportUnhandledSaveRequest) {
// Just log, so it's not thrown again on @After if already thrown on main body
Log.d(TAG, "assertNoUnhandledSaveRequests(): already reported, "
+ "but logging just in case: " + mSaveRequests);
return;
}
mReportUnhandledSaveRequest = false;
throw new AssertionError(mSaveRequests.size() + " unhandled save requests: "
+ mSaveRequests);
}
public void setHandler(Handler handler) {
mHandler = handler;
}
/**
* Resets its internal state.
*/
public void reset() {
mResponses.clear();
mFillRequests.clear();
mSaveRequests.clear();
mExceptions = null;
mOnSaveIntentSender = null;
mAcceptedPackageName = null;
mReportUnhandledFillRequest = true;
mReportUnhandledSaveRequest = true;
}
private void onFillRequest(List<FillContext> contexts, Bundle data,
CancellationSignal cancellationSignal, FillCallback callback, int flags,
InlineSuggestionsRequest inlineRequest, IntentSender delayFillIntentSender,
int requestId) {
try {
CannedFillResponse response = null;
try {
response = mResponses.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted getting CannedResponse: " + e);
Thread.currentThread().interrupt();
addException(e);
return;
}
if (response == null) {
final String activityName = getActivityName(contexts);
final String msg = "onFillRequest() for activity " + activityName
+ " received when no canned response was set.";
dumpStructure(msg, contexts);
return;
}
if (response.getResponseType() == NULL) {
Log.d(TAG, "onFillRequest(): replying with null");
callback.onSuccess(null);
return;
}
if (response.getResponseType() == TIMEOUT) {
Log.d(TAG, "onFillRequest(): not replying at all");
return;
}
if (response.getResponseType() == FAILURE) {
Log.d(TAG, "onFillRequest(): replying with failure");
callback.onFailure("D'OH!");
return;
}
if (response.getResponseType() == ResponseType.NO_MORE) {
Log.w(TAG, "onFillRequest(): replying with null when not expecting more");
addException(new IllegalStateException("got unexpected request"));
callback.onSuccess(null);
return;
}
final String failureMessage = response.getFailureMessage();
if (failureMessage != null) {
Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage);
callback.onFailure(failureMessage);
return;
}
final FillResponse fillResponse;
switch (mIdMode) {
case RESOURCE_ID:
fillResponse = response.asFillResponse(contexts,
(id) -> Helper.findNodeByResourceId(contexts, id));
break;
case HTML_NAME:
fillResponse = response.asFillResponse(contexts,
(name) -> Helper.findNodeByHtmlName(contexts, name));
break;
case HTML_NAME_OR_RESOURCE_ID:
fillResponse = response.asFillResponse(contexts,
(id) -> Helper.findNodeByHtmlNameOrResourceId(contexts, id));
break;
default:
throw new IllegalStateException("Unknown id mode: " + mIdMode);
}
if (response.getResponseType() == ResponseType.DELAY) {
mHandler.postDelayed(() -> {
Log.v(TAG,
"onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
callback.onSuccess(fillResponse);
// Add a fill request to let test case know response was sent.
Helper.offer(mFillRequests,
new FillRequest(contexts, data, cancellationSignal, callback,
flags, inlineRequest, delayFillIntentSender, requestId),
CONNECTION_TIMEOUT.ms());
}, RESPONSE_DELAY_MS);
} else {
Log.v(TAG, "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
callback.onSuccess(fillResponse);
}
} catch (Throwable t) {
addException(t);
} finally {
Helper.offer(mFillRequests, new FillRequest(contexts, data, cancellationSignal,
callback, flags, inlineRequest, delayFillIntentSender, requestId),
CONNECTION_TIMEOUT.ms());
}
}
private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
List<String> datasetIds) {
Log.d(TAG, "onSaveRequest(): sender=" + mOnSaveIntentSender);
try {
if (mOnSaveIntentSender != null) {
callback.onSuccess(mOnSaveIntentSender);
} else {
callback.onSuccess();
}
} finally {
Helper.offer(mSaveRequests, new SaveRequest(contexts, data, callback, datasetIds),
CONNECTION_TIMEOUT.ms());
}
}
private void dump(PrintWriter pw) {
pw.print("mResponses: "); pw.println(mResponses);
pw.print("mFillRequests: "); pw.println(mFillRequests);
pw.print("mSaveRequests: "); pw.println(mSaveRequests);
pw.print("mExceptions: "); pw.println(mExceptions);
pw.print("mOnSaveIntentSender: "); pw.println(mOnSaveIntentSender);
pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
pw.print("mReportUnhandledFillRequest: "); pw.println(mReportUnhandledSaveRequest);
pw.print("mIdMode: "); pw.println(mIdMode);
}
}
}