jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/334371 )

Change subject: Get user ID after logging in or creating an account
......................................................................


Get user ID after logging in or creating an account

The MW API doesn't return user id on login or account creation success.
We have to get it separately.  Currently, we don't do this, and the user id
in the app is always 0.

It seems the numeric user ID is needed for EventLogging, so we'd better
get it.

For the login case, this adds another paramter to the request that's
already made after login to retrieve group memberships, so that the user
ID is also returned.

For account creation, a simple new UserIdClient is added to make the
follow-up request.

Bug: T149915
Change-Id: I3dc7871805edf51d2de6598dd5a5e67140f416b5
---
M app/src/main/java/org/wikipedia/WikipediaApp.java
M app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
M app/src/main/java/org/wikipedia/createaccount/CreateAccountClient.java
M app/src/main/java/org/wikipedia/createaccount/CreateAccountSuccessResult.java
D app/src/main/java/org/wikipedia/login/GroupMembershipClient.java
M app/src/main/java/org/wikipedia/login/LoginActivity.java
M app/src/main/java/org/wikipedia/login/LoginClient.java
M app/src/main/java/org/wikipedia/login/User.java
A app/src/main/java/org/wikipedia/login/UserExtendedInfoClient.java
A app/src/main/java/org/wikipedia/login/UserIdClient.java
M app/src/main/java/org/wikipedia/login/UserInfoStorage.java
M app/src/main/java/org/wikipedia/settings/Prefs.java
M 
app/src/main/java/org/wikipedia/useroption/dataclient/DefaultUserOptionDataClient.java
M app/src/main/java/org/wikipedia/useroption/dataclient/UserInfo.java
M app/src/main/res/values/preference_keys.xml
A app/src/test/java/org/wikipedia/login/UserIdClientTest.java
M app/src/test/java/org/wikipedia/login/UserTest.java
A app/src/test/res/raw/user_info.json
18 files changed, 425 insertions(+), 184 deletions(-)

Approvals:
  Dbrant: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/app/src/main/java/org/wikipedia/WikipediaApp.java 
b/app/src/main/java/org/wikipedia/WikipediaApp.java
index f2c5e81..698801f 100644
--- a/app/src/main/java/org/wikipedia/WikipediaApp.java
+++ b/app/src/main/java/org/wikipedia/WikipediaApp.java
@@ -35,6 +35,7 @@
 import org.wikipedia.dataclient.OkHttpConnectionFactory;
 import org.wikipedia.dataclient.SharedPreferenceCookieManager;
 import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
 import org.wikipedia.edit.summaries.EditSummary;
 import org.wikipedia.events.ChangeTextSizeEvent;
 import org.wikipedia.events.ThemeChangeEvent;
@@ -43,6 +44,7 @@
 import org.wikipedia.language.AppLanguageLookUpTable;
 import org.wikipedia.language.AppLanguageState;
 import org.wikipedia.login.User;
+import org.wikipedia.login.UserIdClient;
 import org.wikipedia.notifications.NotificationPollBroadcastReceiver;
 import org.wikipedia.onboarding.OnboardingStateMachine;
 import org.wikipedia.onboarding.PrefsOnboardingStateMachine;
@@ -73,6 +75,8 @@
 import java.util.Random;
 import java.util.UUID;
 
+import retrofit2.Call;
+
 import static org.apache.commons.lang3.StringUtils.defaultString;
 import static org.wikipedia.util.DimenUtil.getFontSizeFromSp;
 import static org.wikipedia.util.ReleaseUtil.getChannel;
@@ -97,6 +101,7 @@
     private CsrfTokenStorage csrfTokenStorage;
     private String userAgent;
     private WikiSite wiki;
+    @NonNull private UserIdClient idClient = new UserIdClient();
 
     private CrashReporter crashReporter;
     private RefWatcher refWatcher;
@@ -260,7 +265,11 @@
 
     @NonNull
     public String getAppOrSystemLanguageCode() {
-        return appLanguageState.getAppOrSystemLanguageCode();
+        String code = appLanguageState.getAppOrSystemLanguageCode();
+        if (User.isLoggedIn() && !User.getUser().getUserIDLang().equals(code)) 
{
+            updateUserIdForLanguage(code);
+        }
+        return code;
     }
 
     @NonNull
@@ -273,6 +282,28 @@
         resetWikiSite();
     }
 
+    private void updateUserIdForLanguage(@NonNull final String code) {
+        final WikiSite wikiSite = WikiSite.forLanguageCode(code);
+        idClient.request(wikiSite, new UserIdClient.Callback() {
+            @Override
+            public void success(@NonNull 
Call<MwQueryResponse<UserIdClient.QueryUserInfo>> call,
+                                int userId) {
+                User user = User.getUser();
+                if (user != null) {
+                    user.setUserID(userId);
+                    user.setUserIDLang(code);
+                    L.v("Found user ID " + userId + " for " + code);
+                }
+            }
+
+            @Override
+            public void failure(@NonNull 
Call<MwQueryResponse<UserIdClient.QueryUserInfo>> call,
+                                @NonNull Throwable caught) {
+                L.e("Failed to get user ID for " + wikiSite.languageCode(), 
caught);
+            }
+        });
+    }
+
     @Nullable
     public String getAppOrSystemLanguageLocalizedName() {
         return appLanguageState.getAppOrSystemLanguageLocalizedName();
diff --git 
a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java 
b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
index f101268..9581730 100644
--- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
+++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
@@ -46,6 +46,8 @@
 
     public static final String LOGIN_REQUEST_SOURCE = "login_request_source";
     public static final String LOGIN_SESSION_TOKEN = "login_session_token";
+    public static final String CREATE_ACCOUNT_RESULT_USERNAME = "username";
+    public static final String CREATE_ACCOUNT_RESULT_PASSWORD = "password";
 
     private CreateAccountInfoClient createAccountInfoClient;
     private CreateAccountClient createAccountClient;
@@ -262,21 +264,14 @@
                 captchaHandler.isActive() ? captchaHandler.captchaWord() : 
"null",
                 new CreateAccountClient.Callback() {
                     @Override
-                    public void success(@NonNull Call<CreateAccountResponse> 
call, @NonNull CreateAccountSuccessResult result) {
+                    public void success(@NonNull Call<CreateAccountResponse> 
call,
+                                        @NonNull final 
CreateAccountSuccessResult result) {
                         if (!progressDialog.isShowing()) {
                             // no longer attached to activity!
                             return;
                         }
-                        createAccountResult = result;
-                        progressDialog.dismiss();
-                        captchaHandler.cancelCaptcha();
-                        funnel.logSuccess();
-                        hideSoftKeyboard(CreateAccountActivity.this);
-                        Intent resultIntent = new Intent();
-                        resultIntent.putExtra("username", 
result.getUsername());
-                        resultIntent.putExtra("password", 
passwordEdit.getText().toString());
-                        setResult(RESULT_ACCOUNT_CREATED, resultIntent);
-                        finish();
+                        finishWithUserResult(result);
+
                     }
 
                     @Override
@@ -311,4 +306,18 @@
         }
         super.onStop();
     }
+
+    private void finishWithUserResult(@NonNull CreateAccountSuccessResult 
result) {
+        Intent resultIntent = new Intent();
+        resultIntent.putExtra(CREATE_ACCOUNT_RESULT_USERNAME, 
result.getUsername());
+        resultIntent.putExtra(CREATE_ACCOUNT_RESULT_PASSWORD, 
passwordEdit.getText().toString());
+        setResult(RESULT_ACCOUNT_CREATED, resultIntent);
+
+        createAccountResult = result;
+        progressDialog.dismiss();
+        captchaHandler.cancelCaptcha();
+        funnel.logSuccess();
+        hideSoftKeyboard(CreateAccountActivity.this);
+        finish();
+    }
 }
diff --git 
a/app/src/main/java/org/wikipedia/createaccount/CreateAccountClient.java 
b/app/src/main/java/org/wikipedia/createaccount/CreateAccountClient.java
index 5bfce24..1b8c4c9 100644
--- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountClient.java
+++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountClient.java
@@ -49,8 +49,7 @@
                 if (response.isSuccessful()) {
                     if (response.body().hasResult()) {
                         CreateAccountResponse result = response.body();
-                        String status = result.status();
-                        if ("PASS".equals(status)) {
+                        if ("PASS".equals(result.status())) {
                             cb.success(call, new 
CreateAccountSuccessResult(result.user()));
                         } else {
                             cb.failure(call, new 
CreateAccountException(result.message()));
diff --git 
a/app/src/main/java/org/wikipedia/createaccount/CreateAccountSuccessResult.java 
b/app/src/main/java/org/wikipedia/createaccount/CreateAccountSuccessResult.java
index 61827a6..835b75b 100644
--- 
a/app/src/main/java/org/wikipedia/createaccount/CreateAccountSuccessResult.java
+++ 
b/app/src/main/java/org/wikipedia/createaccount/CreateAccountSuccessResult.java
@@ -2,16 +2,17 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.support.annotation.NonNull;
 
-public class CreateAccountSuccessResult extends CreateAccountResult implements 
Parcelable {
-    private final String username;
+class CreateAccountSuccessResult extends CreateAccountResult implements 
Parcelable {
+    private String username;
 
-    public CreateAccountSuccessResult(String username) {
+    CreateAccountSuccessResult(@NonNull String username) {
         super("PASS", "Account created");
         this.username = username;
     }
 
-    public String getUsername() {
+    String getUsername() {
         return username;
     }
 
@@ -21,7 +22,7 @@
         parcel.writeString(username);
     }
 
-    protected CreateAccountSuccessResult(Parcel in) {
+    private CreateAccountSuccessResult(Parcel in) {
         super(in);
         username = in.readString();
     }
diff --git a/app/src/main/java/org/wikipedia/login/GroupMembershipClient.java 
b/app/src/main/java/org/wikipedia/login/GroupMembershipClient.java
deleted file mode 100644
index dcad9e4..0000000
--- a/app/src/main/java/org/wikipedia/login/GroupMembershipClient.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package org.wikipedia.login;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import com.google.gson.annotations.SerializedName;
-
-import org.wikipedia.dataclient.WikiSite;
-import org.wikipedia.dataclient.mwapi.MwQueryResponse;
-import org.wikipedia.dataclient.retrofit.MwCachedService;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-import retrofit2.http.POST;
-import retrofit2.http.Query;
-
-/**
- * Retrofit DataClient to retrieve implicit group membership information for a 
specific user.
- */
-class GroupMembershipClient {
-    @NonNull private final MwCachedService<Service> cachedService = new 
MwCachedService<>(Service.class);
-
-    @Nullable private Call<MwQueryResponse<UserMemberships>> groupCall;
-
-    interface GroupMembershipCallback {
-        void success(@NonNull Set<String> result);
-        void error(@NonNull Throwable caught);
-    }
-
-    public void request(@NonNull final WikiSite wiki, @NonNull final String 
userName,
-                        @NonNull final GroupMembershipCallback cb) {
-        cancel();
-
-        groupCall = cachedService.service(wiki).listUsers(userName);
-        groupCall.enqueue(new Callback<MwQueryResponse<UserMemberships>>() {
-            @Override
-            public void onResponse(Call<MwQueryResponse<UserMemberships>> call,
-                                   Response<MwQueryResponse<UserMemberships>> 
response) {
-                if (response.isSuccessful()) {
-                    final MwQueryResponse<UserMemberships> body = 
response.body();
-                    final UserMemberships query = body.query();
-                    if (query != null) {
-                        cb.success(query.getGroupsFor(userName));
-                    } else if (body.getError() != null) {
-                        cb.error(new LoginClient.LoginFailedException(
-                                "Failed to retrieve group membership data. "
-                                        + body.getError().toString()));
-                    } else {
-                        cb.error(new LoginClient.LoginFailedException(
-                                "Unexpected error trying to retrieve group 
membership data. "
-                                        + body.toString()));
-                    }
-                } else {
-                    cb.error(new 
LoginClient.LoginFailedException(response.message()));
-                }
-            }
-
-            @Override
-            public void onFailure(Call<MwQueryResponse<UserMemberships>> call, 
Throwable caught) {
-                cb.error(caught);
-            }
-        });
-    }
-
-    public void cancel() {
-        cancelTokenRequest();
-    }
-
-    private void cancelTokenRequest() {
-        if (groupCall == null) {
-            return;
-        }
-        groupCall.cancel();
-        groupCall = null;
-    }
-
-    private interface Service {
-
-        /** Request the implicit groups a user belongs to. */
-        @NonNull
-        @POST("w/api.php?format=json&formatversion=2&action=query"
-                + "&list=users&usprop=implicitgroups")
-        Call<MwQueryResponse<UserMemberships>> listUsers(
-                @Query("ususers") @NonNull String userName);
-    }
-
-    private static final class UserMemberships {
-        @SuppressWarnings("MismatchedReadAndWriteOfArray") 
@SerializedName("users") @NonNull
-        private List<ListUsersResponse> users = Collections.emptyList();
-
-        @NonNull Set<String> getGroupsFor(@NonNull String userName) {
-            if (!users.isEmpty()) {
-                for (ListUsersResponse user : users) {
-                    final Set<String> groups = user.getGroupsFor(userName);
-                    if (groups != null) {
-                        return groups;
-                    }
-                }
-            }
-            return Collections.emptySet();
-        }
-
-        private static final class ListUsersResponse {
-            @SerializedName("name") @Nullable private String name;
-
-            @SerializedName("implicitgroups") @Nullable private String[] 
implicitGroups;
-
-            @Nullable Set<String> getGroupsFor(@NonNull String userName) {
-                if (userName.equals(name) && implicitGroups != null) {
-                    Set<String> groups = new HashSet<>();
-                    groups.addAll(Arrays.asList(implicitGroups));
-                    return Collections.unmodifiableSet(groups);
-                } else {
-                    return null;
-                }
-            }
-        }
-    }
-}
diff --git a/app/src/main/java/org/wikipedia/login/LoginActivity.java 
b/app/src/main/java/org/wikipedia/login/LoginActivity.java
index 4a4c26f..0331964 100644
--- a/app/src/main/java/org/wikipedia/login/LoginActivity.java
+++ b/app/src/main/java/org/wikipedia/login/LoginActivity.java
@@ -194,8 +194,8 @@
                 logLoginStart();
             }
             if (resultCode == CreateAccountActivity.RESULT_ACCOUNT_CREATED) {
-                usernameText.setText(data.getStringExtra("username"));
-                passwordText.setText(data.getStringExtra("password"));
+                
usernameText.setText(data.getStringExtra(CreateAccountActivity.CREATE_ACCOUNT_RESULT_USERNAME));
+                
passwordText.setText(data.getStringExtra(CreateAccountActivity.CREATE_ACCOUNT_RESULT_PASSWORD));
                 funnel.logCreateAccountSuccess();
                 FeedbackUtil.showMessage(this,
                         R.string.create_account_account_created_toast);
diff --git a/app/src/main/java/org/wikipedia/login/LoginClient.java 
b/app/src/main/java/org/wikipedia/login/LoginClient.java
index 6909692..addbfa4 100644
--- a/app/src/main/java/org/wikipedia/login/LoginClient.java
+++ b/app/src/main/java/org/wikipedia/login/LoginClient.java
@@ -95,7 +95,7 @@
                             // The server could do some transformations on 
user names, e.g. on some
                             // wikis is uppercases the first letter.
                             String actualUserName = 
loginResult.getUser().getUsername();
-                            getGroupMemberships(wiki, actualUserName, 
loginResult, cb);
+                            getExtendedInfo(wiki, actualUserName, loginResult, 
cb);
                         } else if ("UI".equals(loginResult.getStatus())) {
                             //TODO: Don't just assume this is a 2FA UI result
                             cb.twoFactorPrompt(new 
LoginFailedException(loginResult.getMessage()), loginToken);
@@ -118,20 +118,22 @@
         });
     }
 
-    private void getGroupMemberships(@NonNull WikiSite wiki, @NonNull String 
userName,
-                                     @NonNull final LoginResult loginResult,
-                                     @NonNull final LoginCallback cb) {
-        GroupMembershipClient groupClient = new GroupMembershipClient();
-        groupClient.request(wiki, userName, new 
GroupMembershipClient.GroupMembershipCallback() {
+    private void getExtendedInfo(@NonNull final WikiSite wiki, @NonNull String 
userName,
+                                 @NonNull final LoginResult loginResult, 
@NonNull final LoginCallback cb) {
+        UserExtendedInfoClient infoClient = new UserExtendedInfoClient();
+        infoClient.request(wiki, userName, new 
UserExtendedInfoClient.Callback() {
             @Override
-            public void success(@NonNull Set<String> groups) {
+            public void success(@NonNull 
Call<MwQueryResponse<UserExtendedInfoClient.QueryResult>> call,
+                                int id, @NonNull Set<String> groups) {
                 final User user = loginResult.getUser();
-                User.setUser(new User(user, groups));
+                User.setUser(new User(user, id, wiki.languageCode(), groups));
                 cb.success(loginResult);
+                L.v("Found user ID " + id + " for " + wiki.languageCode());
             }
 
             @Override
-            public void error(@NonNull Throwable caught) {
+            public void failure(@NonNull 
Call<MwQueryResponse<UserExtendedInfoClient.QueryResult>> call,
+                              @NonNull Throwable caught) {
                 L.e("Login suceeded but getting group information failed. " + 
caught);
                 cb.error(caught);
             }
@@ -222,7 +224,7 @@
                 User user = null;
                 String userMessage = null;
                 if ("PASS".equals(status)) {
-                    user = new User(userName, password, 0);
+                    user = new User(userName, password);
                 } else if ("FAIL".equals(status)) {
                     userMessage = message;
                 } else if ("UI".equals(status)) {
diff --git a/app/src/main/java/org/wikipedia/login/User.java 
b/app/src/main/java/org/wikipedia/login/User.java
index b379c60..aaca2c5 100644
--- a/app/src/main/java/org/wikipedia/login/User.java
+++ b/app/src/main/java/org/wikipedia/login/User.java
@@ -4,6 +4,8 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 
+import org.apache.commons.lang3.StringUtils;
+
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -48,22 +50,24 @@
 
     @NonNull private final String username;
     @NonNull private final String password;
-    private final int userID;
+    private int userID;
+    @NonNull private String userIDLang = "";
     @NonNull private final Set<String> groups;
 
-    public User(@NonNull String username, @NonNull String password, int 
userID) {
-        this(username, password, userID, null);
+    public User(@NonNull String username, @NonNull String password) {
+        this(username, password, 0, "", null);
     }
 
-    public User(@NonNull User other, @Nullable Set<String> groups) {
-        this(other.username, other.password, other.userID, groups);
+    public User(@NonNull User other, int id, @NonNull String userIDLang, 
@Nullable Set<String> groups) {
+        this(other.username, other.password, id, userIDLang, groups);
     }
 
     public User(@NonNull String username, @NonNull String password, int userID,
-                @Nullable Set<String> groups) {
+                @NonNull String userIDLang, @Nullable Set<String> groups) {
         this.username = username;
         this.password = password;
         this.userID = userID;
+        this.userIDLang = userIDLang;
         if (groups != null) {
             this.groups = Collections.unmodifiableSet(new HashSet<>(groups));
         } else {
@@ -85,6 +89,18 @@
         return userID;
     }
 
+    @NonNull public String getUserIDLang() {
+        return StringUtils.defaultString(userIDLang, "");
+    }
+
+    public void setUserID(int id) {
+        this.userID = id;
+    }
+
+    public void setUserIDLang(@NonNull String code) {
+        this.userIDLang = code;
+    }
+
     public boolean isAllowed(@NonNull Set<String> allowedGroups) {
         return !allowedGroups.isEmpty() && 
!Collections.disjoint(allowedGroups, groups);
     }
diff --git a/app/src/main/java/org/wikipedia/login/UserExtendedInfoClient.java 
b/app/src/main/java/org/wikipedia/login/UserExtendedInfoClient.java
new file mode 100644
index 0000000..e52087d
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/login/UserExtendedInfoClient.java
@@ -0,0 +1,138 @@
+package org.wikipedia.login;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+import org.wikipedia.dataclient.retrofit.MwCachedService;
+import org.wikipedia.useroption.dataclient.UserInfo;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.http.POST;
+import retrofit2.http.Query;
+
+/**
+ * Retrofit DataClient to retrieve implicit user info and group membership 
information for a specific user.
+ */
+class UserExtendedInfoClient {
+    @NonNull private final MwCachedService<Service> cachedService = new 
MwCachedService<>(Service.class);
+
+    @Nullable private Call<MwQueryResponse<QueryResult>> groupCall;
+
+    interface Callback {
+        void success(@NonNull Call<MwQueryResponse<QueryResult>> call, int id, 
@NonNull Set<String> groups);
+        void failure(@NonNull Call<MwQueryResponse<QueryResult>> call, 
@NonNull Throwable caught);
+    }
+
+    public Call<MwQueryResponse<QueryResult>> request(@NonNull WikiSite wiki, 
@NonNull String userName,
+                                                      @NonNull Callback cb) {
+        return request(cachedService.service(wiki), userName, cb);
+    }
+
+    @VisibleForTesting Call<MwQueryResponse<QueryResult>> request(@NonNull 
Service service,
+                                                                  @NonNull 
final String userName,
+                                                                  @NonNull 
final Callback cb) {
+        cancel();
+
+        groupCall = service.request(userName);
+        groupCall.enqueue(new 
retrofit2.Callback<MwQueryResponse<QueryResult>>() {
+            @Override
+            public void onResponse(Call<MwQueryResponse<QueryResult>> call,
+                                   Response<MwQueryResponse<QueryResult>> 
response) {
+                if (response.isSuccessful()) {
+                    final MwQueryResponse<QueryResult> body = response.body();
+                    final QueryResult query = body.query();
+                    if (response.body().success()) {
+                        cb.success(call, query.id(), 
query.getGroupsFor(userName));
+                    } else if (response.body().hasError()) {
+                        // noinspection ConstantConditions
+                        cb.failure(call, new LoginClient.LoginFailedException(
+                                "Failed to retrieve user ID and group 
membership data. "
+                                        + body.getError().toString()));
+                    } else {
+                        cb.failure(call, new LoginClient.LoginFailedException(
+                                "Unexpected error trying to retrieve user ID 
and group membership data. "
+                                        + body.toString()));
+                    }
+                } else {
+                    cb.failure(call, new 
LoginClient.LoginFailedException(response.message()));
+                }
+            }
+
+            @Override
+            public void onFailure(Call<MwQueryResponse<QueryResult>> call, 
Throwable caught) {
+                cb.failure(call, caught);
+            }
+        });
+        return groupCall;
+    }
+
+    public void cancel() {
+        cancelTokenRequest();
+    }
+
+    private void cancelTokenRequest() {
+        if (groupCall == null) {
+            return;
+        }
+        groupCall.cancel();
+        groupCall = null;
+    }
+
+    static final class QueryResult {
+        @SuppressWarnings("MismatchedReadAndWriteOfArray") 
@SerializedName("users") @NonNull
+        private List<ListUsersResponse> users = Collections.emptyList();
+
+        @SuppressWarnings("unused") @SerializedName("userinfo") private 
UserInfo userInfo;
+
+        int id() {
+            return userInfo.id();
+        }
+
+        @NonNull Set<String> getGroupsFor(@NonNull String userName) {
+            if (!users.isEmpty()) {
+                for (ListUsersResponse user : users) {
+                    final Set<String> groups = user.getGroupsFor(userName);
+                    if (groups != null) {
+                        return groups;
+                    }
+                }
+            }
+            return Collections.emptySet();
+        }
+
+        private static final class ListUsersResponse {
+            @SerializedName("name") @Nullable private String name;
+
+            @SerializedName("implicitgroups") @Nullable private String[] 
implicitGroups;
+
+            @Nullable Set<String> getGroupsFor(@NonNull String userName) {
+                if (userName.equals(name) && implicitGroups != null) {
+                    Set<String> groups = new HashSet<>();
+                    groups.addAll(Arrays.asList(implicitGroups));
+                    return Collections.unmodifiableSet(groups);
+                } else {
+                    return null;
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting interface Service {
+        /** Request the implicit groups a user belongs to. */
+        @NonNull
+        
@POST("w/api.php?action=query&format=json&formatversion=2&meta=userinfo&list=users&usprop=implicitgroups")
+        Call<MwQueryResponse<QueryResult>> request(@Query("ususers") @NonNull 
String userName);
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/login/UserIdClient.java 
b/app/src/main/java/org/wikipedia/login/UserIdClient.java
new file mode 100644
index 0000000..c0e2f51
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/login/UserIdClient.java
@@ -0,0 +1,68 @@
+package org.wikipedia.login;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.dataclient.mwapi.MwException;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+import org.wikipedia.dataclient.retrofit.MwCachedService;
+import org.wikipedia.dataclient.retrofit.RetrofitException;
+import org.wikipedia.useroption.dataclient.UserInfo;
+
+import java.io.IOException;
+
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.http.GET;
+
+public class UserIdClient {
+    @NonNull private final MwCachedService<Service> cachedService = new 
MwCachedService<>(Service.class);
+
+    public interface Callback {
+        void success(@NonNull Call<MwQueryResponse<QueryUserInfo>> call, int 
userId);
+        void failure(@NonNull Call<MwQueryResponse<QueryUserInfo>> call, 
@NonNull Throwable caught);
+    }
+
+    public Call<MwQueryResponse<QueryUserInfo>> request(@NonNull WikiSite 
wiki, @NonNull Callback cb) {
+        return request(cachedService.service(wiki), cb);
+    }
+
+    public Call<MwQueryResponse<QueryUserInfo>> request(@NonNull Service 
service, @NonNull final Callback cb) {
+        Call<MwQueryResponse<QueryUserInfo>> call = service.request();
+        call.enqueue(new retrofit2.Callback<MwQueryResponse<QueryUserInfo>>() {
+            @Override
+            public void onResponse(Call<MwQueryResponse<QueryUserInfo>> call, 
Response<MwQueryResponse<QueryUserInfo>> response) {
+                if (response.isSuccessful()) {
+                    if (response.body().success()) {
+                        cb.success(call, 
response.body().query().userInfo().id());
+                    } else if (response.body().hasError()) {
+                        cb.failure(call, new 
MwException(response.body().getError()));
+                    } else {
+                        cb.failure(call, new IOException("An unknown error 
occurred."));
+                    }
+                } else {
+                    cb.failure(call, RetrofitException.httpError(response, 
cachedService.retrofit()));
+                }
+            }
+
+            @Override
+            public void onFailure(Call<MwQueryResponse<QueryUserInfo>> call, 
Throwable caught) {
+                cb.failure(call, caught);
+            }
+        });
+        return call;
+    }
+
+    public class QueryUserInfo {
+        @SuppressWarnings("unused,NullableProblems") @NonNull private UserInfo 
userinfo;
+        @NonNull UserInfo userInfo() {
+            return userinfo;
+        }
+    }
+
+    @VisibleForTesting interface Service {
+        
@GET("w/api.php?action=query&format=json&formatversion=2&meta=userinfo")
+        @NonNull Call<MwQueryResponse<QueryUserInfo>> request();
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/login/UserInfoStorage.java 
b/app/src/main/java/org/wikipedia/login/UserInfoStorage.java
index a0c6f6b..a4bb238 100644
--- a/app/src/main/java/org/wikipedia/login/UserInfoStorage.java
+++ b/app/src/main/java/org/wikipedia/login/UserInfoStorage.java
@@ -11,6 +11,7 @@
         Prefs.setLoginUsername(user.getUsername());
         Prefs.setLoginPassword(user.getPassword());
         Prefs.setLoginUserId(user.getUserID());
+        Prefs.setLoginUserIdLang(user.getUserIDLang());
         Prefs.setLoginGroups(user.getGroupMemberships());
     }
 
@@ -22,6 +23,7 @@
                     Prefs.getLoginUsername(),
                     Prefs.getLoginPassword(),
                     Prefs.getLoginUserId(),
+                    Prefs.getLoginUserIdLang(),
                     Prefs.getLoginGroups()
             );
         }
@@ -32,6 +34,7 @@
         Prefs.removeLoginUsername();
         Prefs.removeLoginPassword();
         Prefs.removeLoginUserId();
+        Prefs.removeLoginUserIdLang();
         Prefs.removeLoginGroups();
     }
 }
diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.java 
b/app/src/main/java/org/wikipedia/settings/Prefs.java
index c43c127..746d8e0 100644
--- a/app/src/main/java/org/wikipedia/settings/Prefs.java
+++ b/app/src/main/java/org/wikipedia/settings/Prefs.java
@@ -184,6 +184,18 @@
         remove(R.string.preference_key_login_user_id);
     }
 
+    @NonNull public static String getLoginUserIdLang() {
+        return getString(R.string.preference_key_login_user_id_lang, "");
+    }
+
+    public static void setLoginUserIdLang(@NonNull String lang) {
+        setString(R.string.preference_key_login_user_id_lang, lang);
+    }
+
+    public static void removeLoginUserIdLang() {
+        remove(R.string.preference_key_login_user_id_lang);
+    }
+
     @Nullable
     public static String getLoginUsername() {
         return getString(R.string.preference_key_login_username, null);
diff --git 
a/app/src/main/java/org/wikipedia/useroption/dataclient/DefaultUserOptionDataClient.java
 
b/app/src/main/java/org/wikipedia/useroption/dataclient/DefaultUserOptionDataClient.java
index 94145d1..cd43db0 100644
--- 
a/app/src/main/java/org/wikipedia/useroption/dataclient/DefaultUserOptionDataClient.java
+++ 
b/app/src/main/java/org/wikipedia/useroption/dataclient/DefaultUserOptionDataClient.java
@@ -118,7 +118,7 @@
     }
 
     private static class PostResponse extends MwPostResponse {
-        private String options;
+        @SuppressWarnings("unused") private String options;
 
         public String result() {
             return options;
@@ -136,10 +136,9 @@
     }
 
     private static class QueryUserInfo {
-        @SerializedName("userinfo")
-        private UserInfo userInfo;
+        @SuppressWarnings("unused") @SerializedName("userinfo") private 
UserInfo userInfo;
 
-        public UserInfo userInfo() {
+        UserInfo userInfo() {
             return userInfo;
         }
     }
diff --git 
a/app/src/main/java/org/wikipedia/useroption/dataclient/UserInfo.java 
b/app/src/main/java/org/wikipedia/useroption/dataclient/UserInfo.java
index 9e01b25..3a53779 100644
--- a/app/src/main/java/org/wikipedia/useroption/dataclient/UserInfo.java
+++ b/app/src/main/java/org/wikipedia/useroption/dataclient/UserInfo.java
@@ -1,6 +1,7 @@
 package org.wikipedia.useroption.dataclient;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 
 import com.google.gson.annotations.SerializedName;
 
@@ -11,36 +12,30 @@
 import java.util.Map;
 
 public class UserInfo {
-    @SerializedName("name")
-    private String username;
-    private int id;
+    @SuppressWarnings("unused") @SerializedName("name") private String 
username;
+    @SuppressWarnings("unused") private int id;
 
     // Object type is any JSON type.
-    @NonNull private Map<String, ?> options;
+    @SuppressWarnings("unused") @Nullable private Map<String, ?> options;
 
     public int id() {
         return id;
     }
 
-    @NonNull
-    public String username() {
-        return username;
-    }
-
-    @NonNull
-    public Collection<UserOption> userjsOptions() {
+    @NonNull public Collection<UserOption> userjsOptions() {
         Collection<UserOption> ret = new ArrayList<>();
-        for (Map.Entry<String, ?> entry : options.entrySet()) {
-            if (entry.getKey().startsWith("userjs-")) {
-                ret.add(new UserOption(entry.getKey(), (String) 
entry.getValue()));
+        if (options != null) {
+            for (Map.Entry<String, ?> entry : options.entrySet()) {
+                if (entry.getKey().startsWith("userjs-")) {
+                    ret.add(new UserOption(entry.getKey(), (String) 
entry.getValue()));
+                }
             }
         }
         return ret;
     }
 
     // Auto-generated
-    @Override
-    public String toString() {
+    @Override public String toString() {
         return "UserInfo{"
                 + "username='" + username + '\''
                 + ", id=" + id
diff --git a/app/src/main/res/values/preference_keys.xml 
b/app/src/main/res/values/preference_keys.xml
index 552057a..acbef76 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -32,6 +32,7 @@
     <string name="preference_key_login_username">username</string>
     <string name="preference_key_login_password">password</string>
     <string name="preference_key_login_user_id">userID</string>
+    <string name="preference_key_login_user_id_lang">userIDLang</string>
     <string name="preference_key_login_groups">groups</string>
     <string 
name="preference_key_show_developer_settings">showDeveloperSettings</string>
     <string name="preference_key_last_run_time_format">%s-lastrun</string>
diff --git a/app/src/test/java/org/wikipedia/login/UserIdClientTest.java 
b/app/src/test/java/org/wikipedia/login/UserIdClientTest.java
new file mode 100644
index 0000000..229e0cb
--- /dev/null
+++ b/app/src/test/java/org/wikipedia/login/UserIdClientTest.java
@@ -0,0 +1,83 @@
+package org.wikipedia.login;
+
+import android.support.annotation.NonNull;
+
+import com.google.gson.stream.MalformedJsonException;
+
+import org.junit.Test;
+import org.wikipedia.dataclient.mwapi.MwException;
+import org.wikipedia.dataclient.mwapi.MwQueryResponse;
+import org.wikipedia.dataclient.retrofit.RetrofitException;
+import org.wikipedia.test.MockWebServerTest;
+
+import retrofit2.Call;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+public class UserIdClientTest extends MockWebServerTest {
+    private UserIdClient subject = new UserIdClient();
+
+    @Test public void testRequestSuccess() throws Throwable {
+        enqueueFromFile("user_info.json");
+
+        UserIdClient.Callback cb = mock(UserIdClient.Callback.class);
+        Call<MwQueryResponse<UserIdClient.QueryUserInfo>> call = request(cb);
+
+        server().takeRequest();
+        assertCallbackSuccess(call, cb);
+    }
+
+    @Test public void testRequestResponseApiError() throws Throwable {
+        enqueueFromFile("api_error.json");
+
+        UserIdClient.Callback cb = mock(UserIdClient.Callback.class);
+        Call<MwQueryResponse<UserIdClient.QueryUserInfo>> call = request(cb);
+
+        server().takeRequest();
+        assertCallbackFailure(call, cb, MwException.class);
+    }
+
+    @Test public void testRequestResponse404() throws Throwable {
+        enqueue404();
+
+        UserIdClient.Callback cb = mock(UserIdClient.Callback.class);
+        Call<MwQueryResponse<UserIdClient.QueryUserInfo>> call = request(cb);
+
+        server().takeRequest();
+        assertCallbackFailure(call, cb, RetrofitException.class);
+    }
+
+    @Test public void testRequestResponseMalformed() throws Throwable {
+        server().enqueue("┏━┓ ︵  /(^.^/)");
+
+        UserIdClient.Callback cb = mock(UserIdClient.Callback.class);
+        Call<MwQueryResponse<UserIdClient.QueryUserInfo>> call = request(cb);
+
+        server().takeRequest();
+        assertCallbackFailure(call, cb, MalformedJsonException.class);
+    }
+
+    private void assertCallbackSuccess(@NonNull 
Call<MwQueryResponse<UserIdClient.QueryUserInfo>> call,
+                                       @NonNull UserIdClient.Callback cb) {
+        verify(cb).success(eq(call), any(Integer.class));
+        //noinspection unchecked
+        verify(cb, never()).failure(any(Call.class), any(Throwable.class));
+    }
+
+    private void assertCallbackFailure(@NonNull 
Call<MwQueryResponse<UserIdClient.QueryUserInfo>> call,
+                                       @NonNull UserIdClient.Callback cb,
+                                       @NonNull Class<? extends Throwable> 
throwable) {
+        //noinspection unchecked
+        verify(cb, never()).success(any(Call.class), any(Integer.class));
+        verify(cb).failure(eq(call), isA(throwable));
+    }
+
+    private Call<MwQueryResponse<UserIdClient.QueryUserInfo>> request(@NonNull 
UserIdClient.Callback cb) {
+        return subject.request(service(UserIdClient.Service.class), cb);
+    }
+}
diff --git a/app/src/test/java/org/wikipedia/login/UserTest.java 
b/app/src/test/java/org/wikipedia/login/UserTest.java
index fbe2935..9632120 100644
--- a/app/src/test/java/org/wikipedia/login/UserTest.java
+++ b/app/src/test/java/org/wikipedia/login/UserTest.java
@@ -24,7 +24,7 @@
     public void setUp() {
         User.disableStorage();
 
-        User user = new User("name", "pwd", USER_ID, GROUPS);
+        User user = new User("name", "pwd", USER_ID, "test", GROUPS);
         User.setUser(user);
     }
 
@@ -35,6 +35,7 @@
         assertThat(user.getUsername(), is("name"));
         assertThat(user.getPassword(), is("pwd"));
         assertThat(user.getUserID(), is(USER_ID));
+        assertThat(user.getUserIDLang(), is("test"));
         assertThat(user.getGroupMemberships(), is(GROUPS));
     }
 
diff --git a/app/src/test/res/raw/user_info.json 
b/app/src/test/res/raw/user_info.json
new file mode 100644
index 0000000..afe22cc
--- /dev/null
+++ b/app/src/test/res/raw/user_info.json
@@ -0,0 +1,9 @@
+{
+  "batchcomplete": true,
+  "query": {
+    "userinfo": {
+      "id": 24531888,
+      "name": "MHolloway (WMF)"
+    }
+  }
+}
\ No newline at end of file

-- 
To view, visit https://gerrit.wikimedia.org/r/334371
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I3dc7871805edf51d2de6598dd5a5e67140f416b5
Gerrit-PatchSet: 7
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Mholloway <mhollo...@wikimedia.org>
Gerrit-Reviewer: BearND <bsitzm...@wikimedia.org>
Gerrit-Reviewer: Dbrant <dbr...@wikimedia.org>
Gerrit-Reviewer: Mholloway <mhollo...@wikimedia.org>
Gerrit-Reviewer: Niedzielski <sniedziel...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to