Commit 13145d83 authored by Tao Bai's avatar Tao Bai Committed by Commit Bot

[Webview Autofill] Implement Local prediction

Present heuristic type to Android Autofill service.

Bug: 849913
Change-Id: I7e832aa157d6fe5adadc8c8edc2dcc77ecd9effa
Reviewed-on: https://chromium-review.googlesource.com/1174977Reviewed-by: default avatarChangwan Ryu <changwan@chromium.org>
Reviewed-by: default avatarSebastien Seguin-Gagnon <sebsg@chromium.org>
Reviewed-by: default avatarMoe Ahmadi <mahmadi@chromium.org>
Commit-Queue: Tao Bai <michaelbai@chromium.org>
Cr-Commit-Position: refs/heads/master@{#588264}
parent 98b974de
...@@ -87,6 +87,7 @@ public class AwAutofillProvider extends AutofillProvider { ...@@ -87,6 +87,7 @@ public class AwAutofillProvider extends AutofillProvider {
.addAttribute("name", field.mName) .addAttribute("name", field.mName)
.addAttribute("type", field.mType) .addAttribute("type", field.mType)
.addAttribute("label", field.mLabel) .addAttribute("label", field.mLabel)
.addAttribute("ua-autofill-hints", field.mHeuristicType)
.addAttribute("id", field.mId); .addAttribute("id", field.mId);
switch (field.getControlType()) { switch (field.getControlType()) {
......
...@@ -836,8 +836,8 @@ public class AwAutofillTest { ...@@ -836,8 +836,8 @@ public class AwAutofillTest {
TestWebServer webServer = TestWebServer.start(); TestWebServer webServer = TestWebServer.start();
final String data = "<html><head></head><body><form action='a.html' name='formname'>" final String data = "<html><head></head><body><form action='a.html' name='formname'>"
+ "<label>User Name:</label>" + "<label>User Name:</label>"
+ "<input type='text' id='text1' name='username' maxlength='30'" + "<input type='text' id='text1' name='name' maxlength='30'"
+ " placeholder='placeholder@placeholder.com' autocomplete='username name'>" + " placeholder='placeholder@placeholder.com' autocomplete='name given-name'>"
+ "<input type='checkbox' id='checkbox1' name='showpassword'>" + "<input type='checkbox' id='checkbox1' name='showpassword'>"
+ "<select id='select1' name='month'>" + "<select id='select1' name='month'>"
+ "<option value='1'>Jan</option>" + "<option value='1'>Jan</option>"
...@@ -881,14 +881,15 @@ public class AwAutofillTest { ...@@ -881,14 +881,15 @@ public class AwAutofillTest {
TestViewStructure child0 = viewStructure.getChild(0); TestViewStructure child0 = viewStructure.getChild(0);
assertEquals(View.AUTOFILL_TYPE_TEXT, child0.getAutofillType()); assertEquals(View.AUTOFILL_TYPE_TEXT, child0.getAutofillType());
assertEquals("placeholder@placeholder.com", child0.getHint()); assertEquals("placeholder@placeholder.com", child0.getHint());
assertEquals("username", child0.getAutofillHints()[0]); assertEquals("name", child0.getAutofillHints()[0]);
assertEquals("name", child0.getAutofillHints()[1]); assertEquals("given-name", child0.getAutofillHints()[1]);
TestViewStructure.AwHtmlInfo htmlInfo0 = child0.getHtmlInfo(); TestViewStructure.AwHtmlInfo htmlInfo0 = child0.getHtmlInfo();
assertEquals("text", htmlInfo0.getAttribute("type")); assertEquals("text", htmlInfo0.getAttribute("type"));
assertEquals("text1", htmlInfo0.getAttribute("id")); assertEquals("text1", htmlInfo0.getAttribute("id"));
assertEquals("username", htmlInfo0.getAttribute("name")); assertEquals("name", htmlInfo0.getAttribute("name"));
assertEquals("User Name:", htmlInfo0.getAttribute("label")); assertEquals("User Name:", htmlInfo0.getAttribute("label"));
assertEquals("30", htmlInfo0.getAttribute("maxlength")); assertEquals("30", htmlInfo0.getAttribute("maxlength"));
assertEquals("NAME_FIRST", htmlInfo0.getAttribute("ua-autofill-hints"));
// Verify checkbox control filled correctly in ViewStructure. // Verify checkbox control filled correctly in ViewStructure.
TestViewStructure child1 = viewStructure.getChild(1); TestViewStructure child1 = viewStructure.getChild(1);
...@@ -901,6 +902,7 @@ public class AwAutofillTest { ...@@ -901,6 +902,7 @@ public class AwAutofillTest {
assertEquals("showpassword", htmlInfo1.getAttribute("name")); assertEquals("showpassword", htmlInfo1.getAttribute("name"));
assertEquals("", htmlInfo1.getAttribute("label")); assertEquals("", htmlInfo1.getAttribute("label"));
assertNull(htmlInfo1.getAttribute("maxlength")); assertNull(htmlInfo1.getAttribute("maxlength"));
assertNull(htmlInfo1.getAttribute("ua-autofill-hints"));
// Verify select control filled correctly in ViewStructure. // Verify select control filled correctly in ViewStructure.
TestViewStructure child2 = viewStructure.getChild(2); TestViewStructure child2 = viewStructure.getChild(2);
...@@ -1479,6 +1481,69 @@ public class AwAutofillTest { ...@@ -1479,6 +1481,69 @@ public class AwAutofillTest {
} }
} }
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUaAutofillHints() throws Throwable {
TestWebServer webServer = TestWebServer.start();
final String data = "<html><head></head><body><form action='a.html' name='formname'>"
+ "<label for=\"frmAddressB\">Address</label>"
+ "<input name=\"bill-address\" id=\"frmAddressB\">"
+ "<label for=\"frmCityB\">City</label>"
+ "<input name=\"bill-city\" id=\"frmCityB\">"
+ "<label for=\"frmStateB\">State</label>"
+ "<input name=\"bill-state\" id=\"frmStateB\">"
+ "<label for=\"frmZipB\">Zip</label>"
+ "<input name=\"bill-zip\" id=\"frmZipB\">"
+ "<input type='checkbox' id='checkbox1' name='showpassword'>"
+ "<label for=\"frmCountryB\">Country</label>"
+ "<input name=\"bill-country\" id=\"frmCountryB\">"
+ "<input type='submit'>"
+ "</form></body></html>";
final int totalControls = 6;
try {
int cnt = 0;
final String url = webServer.setResponse(FILE, data, null);
loadUrlSync(url);
executeJavaScriptAndWaitForResult("document.getElementById('frmAddressB').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
// Note that we currently call ENTER/EXIT one more time.
cnt += waitForCallbackAndVerifyTypes(cnt,
new Integer[] {AUTOFILL_CANCEL, AUTOFILL_VIEW_ENTERED, AUTOFILL_VIEW_EXITED,
AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(totalControls, viewStructure.getChildCount());
TestViewStructure child0 = viewStructure.getChild(0);
TestViewStructure.AwHtmlInfo htmlInfo0 = child0.getHtmlInfo();
assertEquals("ADDRESS_HOME_LINE1", htmlInfo0.getAttribute("ua-autofill-hints"));
TestViewStructure child1 = viewStructure.getChild(1);
TestViewStructure.AwHtmlInfo htmlInfo1 = child1.getHtmlInfo();
assertEquals("ADDRESS_HOME_CITY", htmlInfo1.getAttribute("ua-autofill-hints"));
TestViewStructure child2 = viewStructure.getChild(2);
TestViewStructure.AwHtmlInfo htmlInfo2 = child2.getHtmlInfo();
assertEquals("ADDRESS_HOME_STATE", htmlInfo2.getAttribute("ua-autofill-hints"));
TestViewStructure child3 = viewStructure.getChild(3);
TestViewStructure.AwHtmlInfo htmlInfo3 = child3.getHtmlInfo();
assertEquals("ADDRESS_HOME_ZIP", htmlInfo3.getAttribute("ua-autofill-hints"));
TestViewStructure child4 = viewStructure.getChild(4);
TestViewStructure.AwHtmlInfo htmlInfo4 = child4.getHtmlInfo();
assertNull(htmlInfo4.getAttribute("ua-autofill-hints"));
TestViewStructure child5 = viewStructure.getChild(5);
TestViewStructure.AwHtmlInfo htmlInfo5 = child5.getHtmlInfo();
assertEquals("ADDRESS_HOME_COUNTRY", htmlInfo5.getAttribute("ua-autofill-hints"));
} finally {
webServer.shutdown();
}
}
@Test @Test
@SmallTest @SmallTest
@Feature({"AndroidWebView"}) @Feature({"AndroidWebView"})
......
...@@ -92,9 +92,15 @@ void AutofillProviderAndroid::StartNewSession(AutofillHandlerProxy* handler, ...@@ -92,9 +92,15 @@ void AutofillProviderAndroid::StartNewSession(AutofillHandlerProxy* handler,
return; return;
} }
FormStructure* form_structure = nullptr;
AutofillField* autofill_field = nullptr;
if (!handler->GetCachedFormAndField(form, field, &form_structure,
&autofill_field)) {
form_structure = nullptr;
}
gfx::RectF transformed_bounding = ToClientAreaBound(bounding_box); gfx::RectF transformed_bounding = ToClientAreaBound(bounding_box);
ScopedJavaLocalRef<jobject> form_obj = form_->GetJavaPeer(); ScopedJavaLocalRef<jobject> form_obj = form_->GetJavaPeer(form_structure);
handler_ = handler->GetWeakPtr(); handler_ = handler->GetWeakPtr();
Java_AutofillProvider_startAutofillSession( Java_AutofillProvider_startAutofillSession(
env, obj, form_obj, index, transformed_bounding.x(), env, obj, form_obj, index, transformed_bounding.x(),
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include "base/android/jni_string.h" #include "base/android/jni_string.h"
#include "components/autofill/android/form_field_data_android.h" #include "components/autofill/android/form_field_data_android.h"
#include "components/autofill/core/browser/form_structure.h"
#include "jni/FormData_jni.h" #include "jni/FormData_jni.h"
using base::android::AttachCurrentThread; using base::android::AttachCurrentThread;
...@@ -30,7 +31,10 @@ FormDataAndroid::~FormDataAndroid() { ...@@ -30,7 +31,10 @@ FormDataAndroid::~FormDataAndroid() {
Java_FormData_onNativeDestroyed(env, obj); Java_FormData_onNativeDestroyed(env, obj);
} }
ScopedJavaLocalRef<jobject> FormDataAndroid::GetJavaPeer() { ScopedJavaLocalRef<jobject> FormDataAndroid::GetJavaPeer(
const FormStructure* form_structure) {
// |form_structure| is ephemeral and shouldn't be used outside this call
// stack.
JNIEnv* env = AttachCurrentThread(); JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
if (obj.is_null()) { if (obj.is_null()) {
...@@ -38,6 +42,8 @@ ScopedJavaLocalRef<jobject> FormDataAndroid::GetJavaPeer() { ...@@ -38,6 +42,8 @@ ScopedJavaLocalRef<jobject> FormDataAndroid::GetJavaPeer() {
fields_.push_back(std::unique_ptr<FormFieldDataAndroid>( fields_.push_back(std::unique_ptr<FormFieldDataAndroid>(
new FormFieldDataAndroid(&form_.fields[i]))); new FormFieldDataAndroid(&form_.fields[i])));
} }
if (form_structure)
ApplyHeuristicFieldType(*form_structure);
ScopedJavaLocalRef<jstring> jname = ScopedJavaLocalRef<jstring> jname =
ConvertUTF16ToJavaString(env, form_.name); ConvertUTF16ToJavaString(env, form_.name);
ScopedJavaLocalRef<jstring> jhost = ScopedJavaLocalRef<jstring> jhost =
...@@ -95,4 +101,17 @@ bool FormDataAndroid::SimilarFormAs(const FormData& form) { ...@@ -95,4 +101,17 @@ bool FormDataAndroid::SimilarFormAs(const FormData& form) {
return form_.SimilarFormAs(form); return form_.SimilarFormAs(form);
} }
void FormDataAndroid::ApplyHeuristicFieldType(
const FormStructure& form_structure) {
DCHECK(form_structure.field_count() == fields_.size());
auto form_field_data_android = fields_.begin();
for (const auto& autofill_field : form_structure) {
DCHECK(form_field_data_android->get()->SimilarFieldAs(*autofill_field));
form_field_data_android->get()->set_heuristic_type(
AutofillType(autofill_field->heuristic_type()));
if (++form_field_data_android == fields_.end())
break;
}
}
} // namespace autofill } // namespace autofill
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
namespace autofill { namespace autofill {
class FormFieldDataAndroid; class FormFieldDataAndroid;
class FormStructure;
// This class is native peer of FormData.java, to make autofill::FormData // This class is native peer of FormData.java, to make autofill::FormData
// available in Java. // available in Java.
...@@ -20,7 +21,8 @@ class FormDataAndroid { ...@@ -20,7 +21,8 @@ class FormDataAndroid {
FormDataAndroid(const FormData& form); FormDataAndroid(const FormData& form);
virtual ~FormDataAndroid(); virtual ~FormDataAndroid();
base::android::ScopedJavaLocalRef<jobject> GetJavaPeer(); base::android::ScopedJavaLocalRef<jobject> GetJavaPeer(
const FormStructure* form_structure);
// Get autofill values from Java side and return FormData. // Get autofill values from Java side and return FormData.
const FormData& GetAutofillValues(); const FormData& GetAutofillValues();
...@@ -46,6 +48,8 @@ class FormDataAndroid { ...@@ -46,6 +48,8 @@ class FormDataAndroid {
// |value|. // |value|.
void OnFormFieldDidChange(size_t index, const base::string16& value); void OnFormFieldDidChange(size_t index, const base::string16& value);
void ApplyHeuristicFieldType(const FormStructure& form);
const FormData& form_for_testing() { return form_; } const FormData& form_for_testing() { return form_; }
private: private:
......
...@@ -22,7 +22,7 @@ using base::android::ToJavaArrayOfStrings; ...@@ -22,7 +22,7 @@ using base::android::ToJavaArrayOfStrings;
namespace autofill { namespace autofill {
FormFieldDataAndroid::FormFieldDataAndroid(FormFieldData* field) FormFieldDataAndroid::FormFieldDataAndroid(FormFieldData* field)
: field_ptr_(field) {} : heuristic_type_(AutofillType(UNKNOWN_TYPE)), field_ptr_(field) {}
ScopedJavaLocalRef<jobject> FormFieldDataAndroid::GetJavaPeer() { ScopedJavaLocalRef<jobject> FormFieldDataAndroid::GetJavaPeer() {
JNIEnv* env = AttachCurrentThread(); JNIEnv* env = AttachCurrentThread();
...@@ -46,12 +46,17 @@ ScopedJavaLocalRef<jobject> FormFieldDataAndroid::GetJavaPeer() { ...@@ -46,12 +46,17 @@ ScopedJavaLocalRef<jobject> FormFieldDataAndroid::GetJavaPeer() {
ToJavaArrayOfStrings(env, field_ptr_->option_values); ToJavaArrayOfStrings(env, field_ptr_->option_values);
ScopedJavaLocalRef<jobjectArray> joption_contents = ScopedJavaLocalRef<jobjectArray> joption_contents =
ToJavaArrayOfStrings(env, field_ptr_->option_contents); ToJavaArrayOfStrings(env, field_ptr_->option_contents);
ScopedJavaLocalRef<jstring> jheuristic_type;
if (!heuristic_type_.IsUnknown())
jheuristic_type =
ConvertUTF8ToJavaString(env, heuristic_type_.ToString());
obj = Java_FormFieldData_createFormFieldData( obj = Java_FormFieldData_createFormFieldData(
env, jname, jlabel, jvalue, jautocomplete_attr, env, jname, jlabel, jvalue, jautocomplete_attr,
field_ptr_->should_autocomplete, jplaceholder, jtype, jid, field_ptr_->should_autocomplete, jplaceholder, jtype, jid,
joption_values, joption_contents, IsCheckable(field_ptr_->check_status), joption_values, joption_contents, IsCheckable(field_ptr_->check_status),
IsChecked(field_ptr_->check_status), field_ptr_->max_length); IsChecked(field_ptr_->check_status), field_ptr_->max_length,
jheuristic_type);
java_ref_ = JavaObjectWeakGlobalRef(env, obj); java_ref_ = JavaObjectWeakGlobalRef(env, obj);
} }
return obj; return obj;
...@@ -88,4 +93,8 @@ void FormFieldDataAndroid::OnFormFieldDidChange(const base::string16& value) { ...@@ -88,4 +93,8 @@ void FormFieldDataAndroid::OnFormFieldDidChange(const base::string16& value) {
ConvertUTF16ToJavaString(env, value)); ConvertUTF16ToJavaString(env, value));
} }
bool FormFieldDataAndroid::SimilarFieldAs(const FormFieldData& field) const {
return field_ptr_->SimilarFieldAs(field);
}
} // namespace autofill } // namespace autofill
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include "base/android/jni_weak_ref.h" #include "base/android/jni_weak_ref.h"
#include "base/android/scoped_java_ref.h" #include "base/android/scoped_java_ref.h"
#include "components/autofill/core/browser/autofill_type.h"
#include "components/autofill/core/common/form_field_data.h" #include "components/autofill/core/common/form_field_data.h"
namespace autofill { namespace autofill {
...@@ -21,8 +22,14 @@ class FormFieldDataAndroid { ...@@ -21,8 +22,14 @@ class FormFieldDataAndroid {
base::android::ScopedJavaLocalRef<jobject> GetJavaPeer(); base::android::ScopedJavaLocalRef<jobject> GetJavaPeer();
void GetValue(); void GetValue();
void OnFormFieldDidChange(const base::string16& value); void OnFormFieldDidChange(const base::string16& value);
bool SimilarFieldAs(const FormFieldData& field) const;
void set_heuristic_type(const AutofillType& heuristic_type) {
heuristic_type_ = heuristic_type;
}
private: private:
AutofillType heuristic_type_;
// Not owned. // Not owned.
FormFieldData* field_ptr_; FormFieldData* field_ptr_;
JavaObjectWeakGlobalRef java_ref_; JavaObjectWeakGlobalRef java_ref_;
......
...@@ -38,6 +38,7 @@ public class FormFieldData { ...@@ -38,6 +38,7 @@ public class FormFieldData {
public final String[] mOptionContents; public final String[] mOptionContents;
public final @ControlType int mControlType; public final @ControlType int mControlType;
public final int mMaxLength; public final int mMaxLength;
public final String mHeuristicType;
private boolean mIsChecked; private boolean mIsChecked;
private String mValue; private String mValue;
...@@ -49,7 +50,7 @@ public class FormFieldData { ...@@ -49,7 +50,7 @@ public class FormFieldData {
private FormFieldData(String name, String label, String value, String autocompleteAttr, private FormFieldData(String name, String label, String value, String autocompleteAttr,
boolean shouldAutocomplete, String placeholder, String type, String id, boolean shouldAutocomplete, String placeholder, String type, String id,
String[] optionValues, String[] optionContents, boolean isCheckField, boolean isChecked, String[] optionValues, String[] optionContents, boolean isCheckField, boolean isChecked,
int maxLength) { int maxLength, String heuristicType) {
mName = name; mName = name;
mLabel = label; mLabel = label;
mValue = value; mValue = value;
...@@ -69,6 +70,7 @@ public class FormFieldData { ...@@ -69,6 +70,7 @@ public class FormFieldData {
mControlType = TYPE_TEXT; mControlType = TYPE_TEXT;
} }
mMaxLength = maxLength; mMaxLength = maxLength;
mHeuristicType = heuristicType;
} }
public @ControlType int getControlType() { public @ControlType int getControlType() {
...@@ -117,9 +119,9 @@ public class FormFieldData { ...@@ -117,9 +119,9 @@ public class FormFieldData {
private static FormFieldData createFormFieldData(String name, String label, String value, private static FormFieldData createFormFieldData(String name, String label, String value,
String autocompleteAttr, boolean shouldAutocomplete, String placeholder, String type, String autocompleteAttr, boolean shouldAutocomplete, String placeholder, String type,
String id, String[] optionValues, String[] optionContents, boolean isCheckField, String id, String[] optionValues, String[] optionContents, boolean isCheckField,
boolean isChecked, int maxLength) { boolean isChecked, int maxLength, String heuristicType) {
return new FormFieldData(name, label, value, autocompleteAttr, shouldAutocomplete, return new FormFieldData(name, label, value, autocompleteAttr, shouldAutocomplete,
placeholder, type, id, optionValues, optionContents, isCheckField, isChecked, placeholder, type, id, optionValues, optionContents, isCheckField, isChecked,
maxLength); maxLength, heuristicType);
} }
} }
...@@ -115,6 +115,15 @@ class AutofillHandler { ...@@ -115,6 +115,15 @@ class AutofillHandler {
AutofillDriver::RendererFormDataAction action, AutofillDriver::RendererFormDataAction action,
const FormData& data); const FormData& data);
// Fills |form_structure| and |autofill_field| with the cached elements
// corresponding to |form| and |field|. This might have the side-effect of
// updating the cache. Returns false if the |form| is not autofillable, or if
// it is not already present in the cache and the cache is full.
bool GetCachedFormAndField(const FormData& form,
const FormFieldData& field,
FormStructure** form_structure,
AutofillField** autofill_field) WARN_UNUSED_RESULT;
// Returns the number of forms this Autofill handler is aware of. // Returns the number of forms this Autofill handler is aware of.
size_t NumFormsDetected() const { return form_structures_.size(); } size_t NumFormsDetected() const { return form_structures_.size(); }
...@@ -162,15 +171,6 @@ class AutofillHandler { ...@@ -162,15 +171,6 @@ class AutofillHandler {
virtual void OnFormsParsed(const std::vector<FormStructure*>& form_structures, virtual void OnFormsParsed(const std::vector<FormStructure*>& form_structures,
const base::TimeTicks timestamp) = 0; const base::TimeTicks timestamp) = 0;
// Fills |form_structure| and |autofill_field| with the cached elements
// corresponding to |form| and |field|. This might have the side-effect of
// updating the cache. Returns false if the |form| is not autofillable, or if
// it is not already present in the cache and the cache is full.
bool GetCachedFormAndField(const FormData& form,
const FormFieldData& field,
FormStructure** form_structure,
AutofillField** autofill_field) WARN_UNUSED_RESULT;
// Fills |form_structure| with a pointer to the cached form structure // Fills |form_structure| with a pointer to the cached form structure
// corresponding to |form_signature|. Returns false if no cached form // corresponding to |form_signature|. Returns false if no cached form
// structure is found with a matching signature. // structure is found with a matching signature.
......
...@@ -65,8 +65,9 @@ void AutofillHandlerProxy::OnSelectControlDidChangeImpl( ...@@ -65,8 +65,9 @@ void AutofillHandlerProxy::OnSelectControlDidChangeImpl(
bool AutofillHandlerProxy::ShouldParseForms(const std::vector<FormData>& forms, bool AutofillHandlerProxy::ShouldParseForms(const std::vector<FormData>& forms,
const base::TimeTicks timestamp) { const base::TimeTicks timestamp) {
provider_->OnFormsSeen(this, forms, timestamp); provider_->OnFormsSeen(this, forms, timestamp);
// Don't use form_structure. // Need to parse the |forms| to FormStructure, so heuristic_type can be
return false; // retrieved later.
return true;
} }
void AutofillHandlerProxy::OnFormsParsed( void AutofillHandlerProxy::OnFormsParsed(
......
...@@ -612,6 +612,9 @@ std::string AutofillType::ToString() const { ...@@ -612,6 +612,9 @@ std::string AutofillType::ToString() const {
// static // static
std::string AutofillType::ServerFieldTypeToString(ServerFieldType type) { std::string AutofillType::ServerFieldTypeToString(ServerFieldType type) {
// You are free to add or remove the String representation of ServerFieldType,
// but don't change any existing values, Android WebView presents them to
// Autofill Service as part of APIs.
switch (type) { switch (type) {
case NO_SERVER_DATA: case NO_SERVER_DATA:
return "NO_SERVER_DATA"; return "NO_SERVER_DATA";
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment