From ce0464ef79f8427d2f6a14a43cc75151548a4e5b Mon Sep 17 00:00:00 2001 From: nuclearfog Date: Sat, 7 Oct 2023 21:59:53 +0200 Subject: [PATCH] added user credentials implementation --- .../twidda/backend/api/Connection.java | 12 +- .../twidda/backend/api/mastodon/Mastodon.java | 18 ++- .../api/mastodon/impl/MastodonAccount.java | 13 +- .../mastodon/impl/MastodonCredentials.java | 105 +++++++++++++++ .../backend/async/CredentialsAction.java | 98 ++++++++++++++ .../twidda/backend/async/UserUpdater.java | 64 --------- .../nuclearfog/twidda/model/Credentials.java | 64 +++++++++ .../twidda/ui/activities/ProfileEditor.java | 123 ++++++++++++------ 8 files changed, 378 insertions(+), 119 deletions(-) create mode 100644 app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonCredentials.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/backend/async/CredentialsAction.java delete mode 100644 app/src/main/java/org/nuclearfog/twidda/backend/async/UserUpdater.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/model/Credentials.java diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java index f5ee8237..bcff6ece 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java @@ -12,6 +12,7 @@ import org.nuclearfog.twidda.backend.helper.update.StatusUpdate; import org.nuclearfog.twidda.backend.helper.update.UserListUpdate; import org.nuclearfog.twidda.backend.helper.update.UserUpdate; import org.nuclearfog.twidda.model.Account; +import org.nuclearfog.twidda.model.Credentials; import org.nuclearfog.twidda.model.Emoji; import org.nuclearfog.twidda.model.Filter; import org.nuclearfog.twidda.model.Hashtag; @@ -539,12 +540,19 @@ public interface Connection { UserList updateUserlist(UserListUpdate update) throws ConnectionException; /** - * updates current user's profile + * get current user's credentials + * + * @return user credentials + */ + Credentials getCredentials() throws ConnectionException; + + /** + * updates current user's credentials * * @param update profile update information * @return updated user information */ - User updateUser(UserUpdate update) throws ConnectionException; + User updateCredentials(UserUpdate update) throws ConnectionException; /** * upload media file and generate a media ID diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java index 144d2a52..6af67029 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java @@ -15,6 +15,7 @@ import org.nuclearfog.twidda.R; import org.nuclearfog.twidda.backend.api.Connection; import org.nuclearfog.twidda.backend.api.ConnectionException; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonAccount; +import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonCredentials; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonEmoji; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonFilter; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonHashtag; @@ -42,6 +43,7 @@ import org.nuclearfog.twidda.backend.utils.ConnectionBuilder; import org.nuclearfog.twidda.backend.utils.StringUtils; import org.nuclearfog.twidda.config.GlobalSettings; import org.nuclearfog.twidda.model.Account; +import org.nuclearfog.twidda.model.Credentials; import org.nuclearfog.twidda.model.Emoji; import org.nuclearfog.twidda.model.Filter; import org.nuclearfog.twidda.model.Hashtag; @@ -220,8 +222,8 @@ public class Mastodon implements Connection { if (response.code() == 200 && body != null) { JSONObject json = new JSONObject(body.string()); String bearer = json.getString("access_token"); - User user = getCredentials(hostname, bearer); - Account account = new MastodonAccount(user, hostname, bearer, connection.getOauthConsumerToken(), connection.getOauthTokenSecret()); + Credentials credentials = getCredentials(hostname, bearer); + Account account = new MastodonAccount(credentials.getId(), hostname, bearer, connection.getOauthConsumerToken(), connection.getOauthTokenSecret()); settings.setLogin(account, false); return account; } @@ -1139,7 +1141,13 @@ public class Mastodon implements Connection { @Override - public User updateUser(UserUpdate update) throws MastodonException { + public Credentials getCredentials() throws ConnectionException { + return getCredentials(settings.getLogin().getHostname(), settings.getLogin().getBearerToken()); + } + + + @Override + public User updateCredentials(UserUpdate update) throws MastodonException { List params = new ArrayList<>(); List streams = new ArrayList<>(); List keys = new ArrayList<>(); @@ -1346,13 +1354,13 @@ public class Mastodon implements Connection { * @param bearer bearer token to use * @return current user information */ - private User getCredentials(String host, @NonNull String bearer) throws MastodonException { + private Credentials getCredentials(String host, @NonNull String bearer) throws MastodonException { try { Response response = get(host, ENDPOINT_VERIFY_CREDENTIALS, bearer, new ArrayList<>()); ResponseBody body = response.body(); if (response.code() == 200 && body != null) { JSONObject json = new JSONObject(body.string()); - return new MastodonUser(json); + return new MastodonCredentials(json); } throw new MastodonException(response); } catch (IOException | JSONException e) { diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonAccount.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonAccount.java index 2d2d9da0..d81cb19d 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonAccount.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonAccount.java @@ -23,23 +23,20 @@ public class MastodonAccount implements Account { private String bearer; private String client_id, client_secret; - private User user; - /** - * @param user user information + * @param id user ID * @param hostname hostname of the Mastodon isntance * @param bearer bearer token * @param client_id app client ID * @param client_secret app client secret */ - public MastodonAccount(User user, String hostname, String bearer, String client_id, String client_secret) { - this.user = user; + public MastodonAccount(long id, String hostname, String bearer, String client_id, String client_secret) { + this.id = id; this.hostname = hostname; this.bearer = bearer; this.client_id = client_id; this.client_secret = client_secret; timestamp = System.currentTimeMillis(); - id = user.getId(); } @@ -58,7 +55,7 @@ public class MastodonAccount implements Account { @Nullable @Override public User getUser() { - return user; + return null; } @@ -107,7 +104,7 @@ public class MastodonAccount implements Account { @NonNull @Override public String toString() { - return "hostname=\"" + getHostname() + "\" configuration=\"" + getConfiguration().getName() + "\" " + user; + return "hostname=\"" + getHostname() + "\" configuration=\"" + getConfiguration().getName() + "\" id=" + id; } diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonCredentials.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonCredentials.java new file mode 100644 index 00000000..1ca116b3 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonCredentials.java @@ -0,0 +1,105 @@ +package org.nuclearfog.twidda.backend.api.mastodon.impl; + +import org.json.JSONException; +import org.json.JSONObject; +import org.nuclearfog.twidda.model.Credentials; +import org.nuclearfog.twidda.model.User.Field; + +/** + * Mastodon implementation of {@link Credentials} + * + * @author nuclearfog + */ +public class MastodonCredentials implements Credentials { + + private static final long serialVersionUID = 4255988093112848364L; + + private long id; + private String username; + private String description; + private String language; + private int visibility; + private boolean sensitive; + + /** + * @param json Credentials json format + */ + public MastodonCredentials(JSONObject json) throws JSONException { + JSONObject sourceJson = json.getJSONObject("source"); + String idStr = json.getString("id"); + String visStr = sourceJson.optString("privacy", ""); + username = json.getString("display_name"); + description = sourceJson.getString("note"); + language = sourceJson.getString("language"); + sensitive = sourceJson.getBoolean("sensitive"); + + switch(visStr) { + case "public": + visibility = PUBLIC; + break; + + case "private": + visibility = PRIVATE; + break; + + case "direct": + visibility = DIRECT; + break; + + case "unlisted": + visibility = UNLISTED; + break; + + default: + visibility = DEFAULT; + break; + } + try { + id = Long.parseLong(idStr); + } catch (NumberFormatException e) { + throw new JSONException("bad user ID:" + idStr); + } + } + + + @Override + public long getId() { + return id; + } + + + @Override + public String getUsername() { + return username; + } + + + @Override + public String getDescription() { + return description; + } + + + @Override + public String getLanguage() { + return language; + } + + + @Override + public int getVisibility() { + return visibility; + } + + + @Override + public boolean isSensitive() { + return sensitive; + } + + + @Override + public Field[] getFields() { + return new Field[0]; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/async/CredentialsAction.java b/app/src/main/java/org/nuclearfog/twidda/backend/async/CredentialsAction.java new file mode 100644 index 00000000..81ef1c27 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/async/CredentialsAction.java @@ -0,0 +1,98 @@ +package org.nuclearfog.twidda.backend.async; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.nuclearfog.twidda.backend.api.Connection; +import org.nuclearfog.twidda.backend.api.ConnectionException; +import org.nuclearfog.twidda.backend.api.ConnectionManager; +import org.nuclearfog.twidda.backend.helper.update.UserUpdate; +import org.nuclearfog.twidda.database.AppDatabase; +import org.nuclearfog.twidda.model.Credentials; +import org.nuclearfog.twidda.model.User; + +/** + * loader class for user credentials + * + * @author nuclearfog + * @see org.nuclearfog.twidda.ui.activities.ProfileEditor + */ +public class CredentialsAction extends AsyncExecutor { + + private Connection connection; + private AppDatabase db; + + /** + * + */ + public CredentialsAction(Context context) { + db = new AppDatabase(context); + connection = ConnectionManager.getDefaultConnection(context); + } + + + @Override + protected Result doInBackground(@NonNull Param param) { + try { + if (param.mode == Param.UPDATE) { + if (param.update != null) { + User user = connection.updateCredentials(param.update); + db.saveUser(user); + param.update.close(); + return new Result(Result.UPDATE, user, null, null); + } + } else if (param.mode == Param.LOAD) { + Credentials credentials = connection.getCredentials(); + return new Result(Result.LOAD, null, credentials, null); + } + } catch (ConnectionException exception) { + return new Result(Result.ERROR, null, null, exception); + } + return null; + } + + /** + * + */ + public static class Param { + + public static final int LOAD = 1; + public static final int UPDATE = 2; + + final int mode; + @Nullable + final UserUpdate update; + + public Param(int mode, @Nullable UserUpdate update) { + this.mode = mode; + this.update = update; + } + } + + /** + * + */ + public static class Result { + + public static final int ERROR = -1; + public static final int LOAD = 10; + public static final int UPDATE = 20; + + public final int mode; + @Nullable + public final User user; + @Nullable + public final Credentials credentials; + @Nullable + public final ConnectionException exception; + + Result(int mode, @Nullable User user, @Nullable Credentials credentials, @Nullable ConnectionException exception) { + this.mode = mode; + this.user = user; + this.credentials = credentials; + this.exception = exception; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/async/UserUpdater.java b/app/src/main/java/org/nuclearfog/twidda/backend/async/UserUpdater.java deleted file mode 100644 index e605974f..00000000 --- a/app/src/main/java/org/nuclearfog/twidda/backend/async/UserUpdater.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.nuclearfog.twidda.backend.async; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.nuclearfog.twidda.backend.api.Connection; -import org.nuclearfog.twidda.backend.api.ConnectionException; -import org.nuclearfog.twidda.backend.api.ConnectionManager; -import org.nuclearfog.twidda.backend.helper.update.UserUpdate; -import org.nuclearfog.twidda.database.AppDatabase; -import org.nuclearfog.twidda.model.User; -import org.nuclearfog.twidda.ui.activities.ProfileEditor; - -/** - * Async loader to update user profile - * - * @author nuclearfog - * @see ProfileEditor - */ -public class UserUpdater extends AsyncExecutor { - - private Connection connection; - private AppDatabase db; - - /** - * - */ - public UserUpdater(Context context) { - db = new AppDatabase(context); - connection = ConnectionManager.getDefaultConnection(context); - } - - - @Override - protected Result doInBackground(@NonNull UserUpdate param) { - try { - User user = connection.updateUser(param); - db.saveUser(user); - return new Result(user, null); - } catch (ConnectionException exception) { - return new Result(null, exception); - } finally { - param.close(); - } - } - - /** - * - */ - public static class Result { - - @Nullable - public final User user; - @Nullable - public final ConnectionException exception; - - Result(@Nullable User user, @Nullable ConnectionException exception) { - this.user = user; - this.exception = exception; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/model/Credentials.java b/app/src/main/java/org/nuclearfog/twidda/model/Credentials.java new file mode 100644 index 00000000..36a9249e --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/model/Credentials.java @@ -0,0 +1,64 @@ +package org.nuclearfog.twidda.model; + +import java.io.Serializable; + +import org.nuclearfog.twidda.model.User.Field; + +/** + * represents credentials of the current user + * + * @author nuclearfog + */ +public interface Credentials extends Serializable { + + int DEFAULT = 0; + + int PUBLIC = 10; + + int PRIVATE = 11; + + int DIRECT = 12; + + int UNLISTED = 13; + + /** + * get user ID + * + * @return user ID + */ + long getId(); + + /** + * get user display name + */ + String getUsername(); + + /** + * get user description + */ + String getDescription(); + + /** + * get default language for posting a status + * + * @return ISO 639 Part 1 two-letter language code or empty + */ + String getLanguage(); + + /** + * get default visibility of the user's status + * + * @return {@link #PUBLIC,#PRIVATE,#DIRECT,#UNLISTED} + */ + int getVisibility(); + + /** + * get default sensitive setting for posts + */ + boolean isSensitive(); + + /** + * get user fields + */ + Field[] getFields(); +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileEditor.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileEditor.java index a0d725c6..b52d21f1 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileEditor.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileEditor.java @@ -6,6 +6,8 @@ import android.content.res.Resources; import android.location.Location; import android.net.Uri; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; @@ -28,7 +30,7 @@ import com.squareup.picasso.Transformation; import org.nuclearfog.twidda.R; import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback; -import org.nuclearfog.twidda.backend.async.UserUpdater; +import org.nuclearfog.twidda.backend.async.CredentialsAction; import org.nuclearfog.twidda.backend.helper.MediaStatus; import org.nuclearfog.twidda.backend.helper.update.UserUpdate; import org.nuclearfog.twidda.backend.image.PicassoBuilder; @@ -37,6 +39,7 @@ import org.nuclearfog.twidda.backend.utils.ErrorUtils; import org.nuclearfog.twidda.backend.utils.ToolbarUpdater; import org.nuclearfog.twidda.config.Configuration; import org.nuclearfog.twidda.config.GlobalSettings; +import org.nuclearfog.twidda.model.Credentials; import org.nuclearfog.twidda.model.User; import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog; import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog.OnConfirmListener; @@ -51,7 +54,7 @@ import jp.wasabeef.picasso.transformations.RoundedCornersTransformation; * * @author nuclearfog */ -public class ProfileEditor extends MediaActivity implements OnClickListener, AsyncCallback, OnConfirmListener, Callback { +public class ProfileEditor extends MediaActivity implements OnClickListener, AsyncCallback, OnConfirmListener, TextWatcher, Callback { /** * key to preload user data @@ -64,7 +67,7 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy */ public static final int RETURN_PROFILE_UPDATED = 0xF5C0E570; - private UserUpdater editorAsync; + private CredentialsAction credentialAction; private GlobalSettings settings; private Picasso picasso; @@ -77,7 +80,10 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy @Nullable private User user; + @Nullable + private Credentials credentials; private UserUpdate userUpdate = new UserUpdate(); + private boolean changed = false; @Override @@ -106,7 +112,7 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy progressDialog = new ProgressDialog(this, null); confirmDialog = new ConfirmDialog(this, this); - editorAsync = new UserUpdater(this); + credentialAction = new CredentialsAction(this); settings = GlobalSettings.get(this); picasso = PicassoBuilder.get(this); @@ -136,35 +142,43 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy Object data = getIntent().getSerializableExtra(KEY_USER); if (data instanceof User) { - setUser((User) data); + user = (User) data; + setUser(); } + username.addTextChangedListener(this); + profileUrl.addTextChangedListener(this); + profileLocation.addTextChangedListener(this); + userDescription.addTextChangedListener(this); profile_image.setOnClickListener(this); profile_banner.setOnClickListener(this); addBannerBtn.setOnClickListener(this); } + @Override + protected void onStart() { + super.onStart(); + if (credentials == null) { + CredentialsAction.Param param = new CredentialsAction.Param(CredentialsAction.Param.LOAD, null); + credentialAction.execute(param, this); + } + } + + @Override protected void onDestroy() { progressDialog.dismiss(); - editorAsync.cancel(); + credentialAction.cancel(); super.onDestroy(); } @Override public void onBackPressed() { - String username = this.username.getText().toString(); - String userLink = profileUrl.getText().toString(); - String userLoc = profileLocation.getText().toString(); - String userBio = userDescription.getText().toString(); - if (user != null && username.equals(user.getUsername()) && userLink.equals(user.getProfileUrl()) - && userLoc.equals(user.getLocation()) && userBio.equals(user.getDescription()) && !userUpdate.imageAdded()) { - super.onBackPressed(); - } else if (username.isEmpty() && userLink.isEmpty() && userLoc.isEmpty() && userBio.isEmpty()) { - super.onBackPressed(); - } else { + if (changed) { confirmDialog.show(ConfirmDialog.PROFILE_EDITOR_LEAVE); + } else { + super.onBackPressed(); } } @@ -227,6 +241,22 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy } + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + + @Override + public void afterTextChanged(Editable s) { + changed = true; + } + + @Override public void onConfirm(int type, boolean remember) { // leave without settings @@ -241,14 +271,17 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy @Override - public void onResult(@NonNull UserUpdater.Result result) { - if (result.user != null) { + public void onResult(@NonNull CredentialsAction.Result result) { + if (result.mode == CredentialsAction.Result.UPDATE) { Intent data = new Intent(); data.putExtra(KEY_USER, result.user); Toast.makeText(getApplicationContext(), R.string.info_profile_updated, Toast.LENGTH_SHORT).show(); setResult(RETURN_PROFILE_UPDATED, data); finish(); - } else { + } else if (result.mode == CredentialsAction.Result.LOAD) { + credentials = result.credentials; + setCredentials(); + } else if (result.mode == CredentialsAction.Result.ERROR) { String message = ErrorUtils.getErrorMessage(this, result.exception); confirmDialog.show(ConfirmDialog.PROFILE_EDITOR_ERROR, message); progressDialog.dismiss(); @@ -273,7 +306,7 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy * update user information */ private void updateUser() { - if (editorAsync.isIdle()) { + if (credentialAction.isIdle()) { String username = this.username.getText().toString(); String userLoc = profileLocation.getText().toString(); String userBio = userDescription.getText().toString(); @@ -283,7 +316,8 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy } else { userUpdate.setProfile(username, userBio, userLoc); if (userUpdate.prepare(getContentResolver())) { - editorAsync.execute(userUpdate, this); + CredentialsAction.Param param = new CredentialsAction.Param(CredentialsAction.Param.UPDATE, userUpdate); + credentialAction.execute(param, this); progressDialog.show(); } else { Toast.makeText(getApplicationContext(), R.string.error_media_init, Toast.LENGTH_SHORT).show(); @@ -295,25 +329,34 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy /** * Set current user's information */ - private void setUser(User user) { - String profileImageUrl = user.getProfileImageThumbnailUrl(); - String bannerImageUrl = user.getBannerImageThumbnailUrl(); - if (!profileImageUrl.isEmpty()) { - Transformation roundCorner = new RoundedCornersTransformation(5, 0); - picasso.load(profileImageUrl).transform(roundCorner).into(profile_image); + private void setUser() { + if (user != null) { + String profileImageUrl = user.getProfileImageThumbnailUrl(); + String bannerImageUrl = user.getBannerImageThumbnailUrl(); + if (!profileImageUrl.isEmpty()) { + Transformation roundCorner = new RoundedCornersTransformation(5, 0); + picasso.load(profileImageUrl).transform(roundCorner).into(profile_image); + } + if (!bannerImageUrl.isEmpty()) { + picasso.load(bannerImageUrl).into(profile_banner, this); + addBannerBtn.setVisibility(View.INVISIBLE); + changeBannerBtn.setVisibility(View.VISIBLE); + } else { + addBannerBtn.setVisibility(View.VISIBLE); + changeBannerBtn.setVisibility(View.INVISIBLE); + } + username.setText(user.getUsername()); + profileUrl.setText(user.getProfileUrl()); + profileLocation.setText(user.getLocation()); + userDescription.setText(user.getDescription()); } - if (!bannerImageUrl.isEmpty()) { - picasso.load(bannerImageUrl).into(profile_banner, this); - addBannerBtn.setVisibility(View.INVISIBLE); - changeBannerBtn.setVisibility(View.VISIBLE); - } else { - addBannerBtn.setVisibility(View.VISIBLE); - changeBannerBtn.setVisibility(View.INVISIBLE); - } - username.setText(user.getUsername()); - profileUrl.setText(user.getProfileUrl()); - profileLocation.setText(user.getLocation()); - userDescription.setText(user.getDescription()); - this.user = user; + } + + /** + * + * set current user's credentials + */ + private void setCredentials() { + } } \ No newline at end of file