Merge pull request #507 from ratabb/rbb/edit_profile

feat: add Edit Profile
This commit is contained in:
Docile-Alligator 2021-11-02 22:06:38 +08:00 committed by GitHub
commit 3a8c6b0a28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1837 additions and 4 deletions

View File

@ -394,6 +394,11 @@
android:parentActivityName=".activities.MainActivity" android:parentActivityName=".activities.MainActivity"
android:theme="@style/AppTheme.SlidableWithTranslucentWindow" /> android:theme="@style/AppTheme.SlidableWithTranslucentWindow" />
<activity android:name=".activities.EditProfileActivity"
android:parentActivityName=".activities.MainActivity"
android:theme="@style/AppTheme.SlidableWithTranslucentWindow"
android:windowSoftInputMode="adjustPan" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="ml.docilealligator.infinityforreddit.provider" android:authorities="ml.docilealligator.infinityforreddit.provider"
@ -420,6 +425,10 @@
<service <service
android:name=".services.MaterialYouService" android:name=".services.MaterialYouService"
android:exported="false" /> android:exported="false" />
<service
android:name=".services.EditProfileService"
android:enabled="true"
android:exported="false" />
</application> </application>
</manifest> </manifest>

View File

@ -14,6 +14,7 @@ import ml.docilealligator.infinityforreddit.activities.CustomizePostFilterActivi
import ml.docilealligator.infinityforreddit.activities.CustomizeThemeActivity; import ml.docilealligator.infinityforreddit.activities.CustomizeThemeActivity;
import ml.docilealligator.infinityforreddit.activities.EditCommentActivity; import ml.docilealligator.infinityforreddit.activities.EditCommentActivity;
import ml.docilealligator.infinityforreddit.activities.EditMultiRedditActivity; import ml.docilealligator.infinityforreddit.activities.EditMultiRedditActivity;
import ml.docilealligator.infinityforreddit.activities.EditProfileActivity;
import ml.docilealligator.infinityforreddit.activities.EditPostActivity; import ml.docilealligator.infinityforreddit.activities.EditPostActivity;
import ml.docilealligator.infinityforreddit.activities.FetchRandomSubredditOrPostActivity; import ml.docilealligator.infinityforreddit.activities.FetchRandomSubredditOrPostActivity;
import ml.docilealligator.infinityforreddit.activities.FilteredPostsActivity; import ml.docilealligator.infinityforreddit.activities.FilteredPostsActivity;
@ -78,6 +79,7 @@ import ml.docilealligator.infinityforreddit.fragments.ViewRedditGalleryImageOrGi
import ml.docilealligator.infinityforreddit.fragments.ViewRedditGalleryVideoFragment; import ml.docilealligator.infinityforreddit.fragments.ViewRedditGalleryVideoFragment;
import ml.docilealligator.infinityforreddit.services.DownloadMediaService; import ml.docilealligator.infinityforreddit.services.DownloadMediaService;
import ml.docilealligator.infinityforreddit.services.DownloadRedditVideoService; import ml.docilealligator.infinityforreddit.services.DownloadRedditVideoService;
import ml.docilealligator.infinityforreddit.services.EditProfileService;
import ml.docilealligator.infinityforreddit.services.MaterialYouService; import ml.docilealligator.infinityforreddit.services.MaterialYouService;
import ml.docilealligator.infinityforreddit.services.SubmitPostService; import ml.docilealligator.infinityforreddit.services.SubmitPostService;
import ml.docilealligator.infinityforreddit.settings.AdvancedPreferenceFragment; import ml.docilealligator.infinityforreddit.settings.AdvancedPreferenceFragment;
@ -286,4 +288,8 @@ public interface AppComponent {
void inject(WikiActivity wikiActivity); void inject(WikiActivity wikiActivity);
void inject(Infinity infinity); void inject(Infinity infinity);
void inject(EditProfileService editProfileService);
void inject(EditProfileActivity editProfileActivity);
} }

View File

@ -37,7 +37,7 @@ import ml.docilealligator.infinityforreddit.user.UserData;
@Database(entities = {Account.class, SubredditData.class, SubscribedSubredditData.class, UserData.class, @Database(entities = {Account.class, SubredditData.class, SubscribedSubredditData.class, UserData.class,
SubscribedUserData.class, MultiReddit.class, CustomTheme.class, RecentSearchQuery.class, SubscribedUserData.class, MultiReddit.class, CustomTheme.class, RecentSearchQuery.class,
ReadPost.class, PostFilter.class, PostFilterUsage.class, AnonymousMultiredditSubreddit.class}, version = 21) ReadPost.class, PostFilter.class, PostFilterUsage.class, AnonymousMultiredditSubreddit.class}, version = 22)
public abstract class RedditDataRoomDatabase extends RoomDatabase { public abstract class RedditDataRoomDatabase extends RoomDatabase {
private static RedditDataRoomDatabase INSTANCE; private static RedditDataRoomDatabase INSTANCE;
@ -51,7 +51,8 @@ public abstract class RedditDataRoomDatabase extends RoomDatabase {
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9,
MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13,
MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17,
MIGRATION_17_18, MIGRATION_18_19, MIGRATION_19_20, MIGRATION_20_21) MIGRATION_17_18, MIGRATION_18_19, MIGRATION_19_20, MIGRATION_20_21,
MIGRATION_21_22)
.build(); .build();
} }
} }
@ -358,4 +359,10 @@ public abstract class RedditDataRoomDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE post_filter ADD COLUMN contain_domains TEXT"); database.execSQL("ALTER TABLE post_filter ADD COLUMN contain_domains TEXT");
} }
}; };
private static final Migration MIGRATION_21_22 = new Migration(21, 22) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE users ADD COLUMN title TEXT");
}
};
} }

View File

@ -0,0 +1,351 @@
package ml.docilealligator.infinityforreddit.activities;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout.LayoutParams;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.r0adkll.slidr.Slidr;
import com.r0adkll.slidr.model.SlidrInterface;
import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
import ml.docilealligator.infinityforreddit.Infinity;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
import ml.docilealligator.infinityforreddit.events.SubmitChangeAvatarEvent;
import ml.docilealligator.infinityforreddit.events.SubmitChangeBannerEvent;
import ml.docilealligator.infinityforreddit.events.SubmitSaveProfileEvent;
import ml.docilealligator.infinityforreddit.services.EditProfileService;
import ml.docilealligator.infinityforreddit.user.UserViewModel;
import ml.docilealligator.infinityforreddit.utils.EditProfileUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import retrofit2.Retrofit;
import javax.inject.Inject;
import javax.inject.Named;
public class EditProfileActivity extends BaseActivity {
private static final int PICK_IMAGE_BANNER_REQUEST_CODE = 0x401;
private static final int PICK_IMAGE_AVATAR_REQUEST_CODE = 0x402;
@BindView(R.id.root_layout_view_edit_profile_activity)
LinearLayout root;
@BindView(R.id.content_view_edit_profile_activity)
LinearLayout content;
@BindView(R.id.toolbar_view_edit_profile_activity)
Toolbar toolbar;
@BindView(R.id.appbar_layout_view_edit_profile_activity)
AppBarLayout appBarLayout;
@BindView(R.id.image_view_banner_edit_profile_activity)
ImageView bannerImageView;
@BindView(R.id.image_view_avatar_edit_profile_activity)
ImageView avatarImageView;
@BindView(R.id.image_view_change_banner_edit_profile_activity)
ImageView changeBanner;
@BindView(R.id.image_view_change_avatar_edit_profile_activity)
ImageView changeAvatar;
@BindView(R.id.edit_text_display_name_edit_profile_activity)
EditText editTextDisplayName;
@BindView(R.id.edit_text_about_you_edit_profile_activity)
EditText editTextAboutYou;
@Inject
@Named("current_account")
SharedPreferences mCurrentAccountSharedPreferences;
@Inject
@Named("default")
SharedPreferences mSharedPreferences;
@Inject
@Named("oauth")
Retrofit mOauthRetrofit;
@Inject
RedditDataRoomDatabase mRedditDataRoomDatabase;
@Inject
CustomThemeWrapper mCustomThemeWrapper;
//
private String mAccountName;
private String mAccessToken;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
((Infinity) getApplication()).getAppComponent().inject(this);
setTransparentStatusBarAfterToolbarCollapsed();
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_profile);
ButterKnife.bind(this);
EventBus.getDefault().register(this);
applyCustomTheme();
adjustToolbar(toolbar);
setSupportActionBar(toolbar);
if (mSharedPreferences.getBoolean(SharedPreferencesUtils.SWIPE_RIGHT_TO_GO_BACK, true)) {
SlidrInterface slidrInterface = Slidr.attach(this);
slidrInterface.unlock();
}
mAccessToken = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCESS_TOKEN, null);
mAccountName = mCurrentAccountSharedPreferences.getString(SharedPreferencesUtils.ACCOUNT_NAME, null);
changeBanner.setOnClickListener(view -> {
startPickImage(PICK_IMAGE_BANNER_REQUEST_CODE);
});
changeAvatar.setOnClickListener(view -> {
startPickImage(PICK_IMAGE_AVATAR_REQUEST_CODE);
});
final RequestManager glide = Glide.with(this);
final UserViewModel.Factory userViewModelFactory =
new UserViewModel.Factory(getApplication(), mRedditDataRoomDatabase, mAccountName);
final UserViewModel userViewModel =
new ViewModelProvider(this, userViewModelFactory).get(UserViewModel.class);
userViewModel.getUserLiveData().observe(this, userData -> {
if (userData == null) return;//
// BANNER
final String userBanner = userData.getBanner();
LayoutParams cBannerLp = (LayoutParams) changeBanner.getLayoutParams();
if (userBanner == null || userBanner.isEmpty()) {
changeBanner.setLongClickable(false);
changeBanner.setImageResource(R.drawable.ic_add_day_night_24dp);
changeBanner.setLayoutParams(new LayoutParams(cBannerLp.width, cBannerLp.height, Gravity.CENTER));
changeBanner.setOnLongClickListener(v -> false);
} else {
changeBanner.setLongClickable(true);
changeBanner.setImageResource(R.drawable.ic_outline_add_a_photo_day_night_24dp);
changeBanner.setLayoutParams(new LayoutParams(cBannerLp.width, cBannerLp.height, Gravity.END | Gravity.BOTTOM));
glide.load(userBanner).into(bannerImageView);
changeBanner.setOnLongClickListener(view -> {
if (mAccessToken == null) return false;
new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme)
.setTitle(R.string.remove_banner)
.setMessage(R.string.are_you_sure)
.setPositiveButton(R.string.yes, (dialogInterface, i)
-> EditProfileUtils.deleteBanner(mOauthRetrofit,
mAccessToken,
mAccountName,
new EditProfileUtils.EditProfileUtilsListener() {
@Override
public void success() {
Toast.makeText(EditProfileActivity.this,
R.string.message_remove_banner_success,
Toast.LENGTH_SHORT).show();
bannerImageView.setImageDrawable(null);//
}
@Override
public void failed(String message) {
Toast.makeText(EditProfileActivity.this,
getString(R.string.message_remove_banner_failed_fmt, message),
Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton(R.string.no, null)
.show();
return true;
});
}
// AVATAR
final String userAvatar = userData.getIconUrl();
glide.load(userAvatar)
.apply(RequestOptions.bitmapTransform(new RoundedCornersTransformation(216, 0)))
.into(avatarImageView);
LayoutParams cAvatarLp = (LayoutParams) changeAvatar.getLayoutParams();
if (userAvatar.contains("avatar_default_")) {
changeAvatar.setLongClickable(false);
changeAvatar.setImageResource(R.drawable.ic_add_day_night_24dp);
changeAvatar.setLayoutParams(new LayoutParams(cAvatarLp.width, cAvatarLp.height, Gravity.CENTER));
changeAvatar.setOnLongClickListener(v -> false);
} else {
changeAvatar.setLongClickable(true);
changeAvatar.setImageResource(R.drawable.ic_outline_add_a_photo_day_night_24dp);
changeAvatar.setLayoutParams(new LayoutParams(cAvatarLp.width, cAvatarLp.height, Gravity.END | Gravity.BOTTOM));
changeAvatar.setOnLongClickListener(view -> {
if (mAccessToken == null) return false;
new MaterialAlertDialogBuilder(this, R.style.MaterialAlertDialogTheme)
.setTitle(R.string.remove_avatar)
.setMessage(R.string.are_you_sure)
.setPositiveButton(R.string.yes, (dialogInterface, i)
-> EditProfileUtils.deleteAvatar(mOauthRetrofit,
mAccessToken,
mAccountName,
new EditProfileUtils.EditProfileUtilsListener() {
@Override
public void success() {
Toast.makeText(EditProfileActivity.this,
R.string.message_remove_avatar_success,
Toast.LENGTH_SHORT).show();//
}
@Override
public void failed(String message) {
Toast.makeText(EditProfileActivity.this,
getString(R.string.message_remove_avatar_failed_fmt, message),
Toast.LENGTH_SHORT).show();
}
}))
.setNegativeButton(R.string.no, null)
.show();
return true;
});
}
editTextAboutYou.setText(userData.getDescription());
editTextDisplayName.setText(userData.getTitle());
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK || data == null) return; //
if (mAccessToken == null || mAccountName == null) return; //
Intent intent = new Intent(this, EditProfileService.class);
intent.setData(data.getData());
intent.putExtra(EditProfileService.EXTRA_ACCOUNT_NAME, mAccountName);
intent.putExtra(EditProfileService.EXTRA_ACCESS_TOKEN, mAccessToken);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
switch (requestCode) {
case PICK_IMAGE_BANNER_REQUEST_CODE:
intent.putExtra(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_CHANGE_BANNER);
ContextCompat.startForegroundService(this, intent);
break;
case PICK_IMAGE_AVATAR_REQUEST_CODE:
intent.putExtra(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_CHANGE_AVATAR);
ContextCompat.startForegroundService(this, intent);
break;
default:
break;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.edit_profile_activity, menu);
applyMenuItemTheme(menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.action_save_edit_profile_activity) {
String displayName = null;
if (editTextDisplayName.getText() != null) {
displayName = editTextDisplayName.getText().toString();
}
String aboutYou = null;
if (editTextAboutYou.getText() != null) {
aboutYou = editTextAboutYou.getText().toString();
}
if (aboutYou == null || displayName == null) return false; //
Intent intent = new Intent(this, EditProfileService.class);
intent.putExtra(EditProfileService.EXTRA_ACCOUNT_NAME, mAccountName);
intent.putExtra(EditProfileService.EXTRA_ACCESS_TOKEN, mAccessToken);
intent.putExtra(EditProfileService.EXTRA_DISPLAY_NAME, displayName); //
intent.putExtra(EditProfileService.EXTRA_ABOUT_YOU, aboutYou); //
intent.putExtra(EditProfileService.EXTRA_POST_TYPE, EditProfileService.EXTRA_POST_TYPE_SAVE_EDIT_PROFILE);
ContextCompat.startForegroundService(this, intent);
return true;
}
return false;
}
@Subscribe
public void onSubmitChangeAvatar(SubmitChangeAvatarEvent event) {
if (event.isSuccess) {
Toast.makeText(this, R.string.message_change_avatar_success, Toast.LENGTH_SHORT).show();
} else {
String message = getString(R.string.message_change_avatar_failed_fmt, event.errorMessage);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
@Subscribe
public void onSubmitChangeBanner(SubmitChangeBannerEvent event) {
if (event.isSuccess) {
Toast.makeText(this, R.string.message_change_banner_success, Toast.LENGTH_SHORT).show();
} else {
String message = getString(R.string.message_change_banner_failed_fmt, event.errorMessage);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
@Subscribe
public void onSubmitSaveProfile(SubmitSaveProfileEvent event) {
if (event.isSuccess) {
Toast.makeText(this, R.string.message_save_profile_success, Toast.LENGTH_SHORT).show();
} else {
String message = getString(R.string.message_save_profile_failed_fmt, event.errorMessage);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
@Override
protected SharedPreferences getDefaultSharedPreferences() {
return mSharedPreferences;
}
@Override
protected CustomThemeWrapper getCustomThemeWrapper() {
return mCustomThemeWrapper;
}
@Override
protected void applyCustomTheme() { //
applyAppBarLayoutAndToolbarTheme(appBarLayout, toolbar);
root.setBackgroundColor(mCustomThemeWrapper.getBackgroundColor());
changeColorTextView(content, mCustomThemeWrapper.getPrimaryTextColor());
}
private void changeColorTextView(ViewGroup viewGroup, int color) {
final int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = viewGroup.getChildAt(i);
if (child instanceof ViewGroup) {
changeColorTextView((ViewGroup) child, color);
} else if (child instanceof TextView) {
((TextView) child).setTextColor(color);
}
}
}
private void startPickImage(int requestId) {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(
Intent.createChooser(intent, getString(R.string.select_from_gallery)),
requestId);
}
}

View File

@ -1022,6 +1022,13 @@ public class ViewUserDetailActivity extends BaseActivity implements SortTypeSele
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.view_user_detail_activity, menu); getMenuInflater().inflate(R.menu.view_user_detail_activity, menu);
if(mAccountName.equals(username)){ // Hide some menus
menu.findItem(R.id.action_send_private_message_view_user_detail_activity).setVisible(false);
menu.findItem(R.id.action_report_view_user_detail_activity).setVisible(false);
menu.findItem(R.id.action_block_user_view_user_detail_activity).setVisible(false);
}else { // Hide Edit Profile menu
menu.findItem(R.id.action_edit_profile_view_user_detail_activity).setVisible(false);
}
applyMenuItemTheme(menu); applyMenuItemTheme(menu);
return true; return true;
} }
@ -1110,6 +1117,9 @@ public class ViewUserDetailActivity extends BaseActivity implements SortTypeSele
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)
.show(); .show();
return true; return true;
} else if(itemId == R.id.action_edit_profile_view_user_detail_activity){
startActivity(new Intent(this, EditProfileActivity.class));
return true;
} }
return false; return false;
} }

View File

@ -403,4 +403,24 @@ public interface RedditAPI {
@GET("{multipath}.json?raw_json=1") @GET("{multipath}.json?raw_json=1")
ListenableFuture<Response<String>> getMultiRedditPostsOauthListenableFuture(@Path(value = "multipath", encoded = true) String multiPath, ListenableFuture<Response<String>> getMultiRedditPostsOauthListenableFuture(@Path(value = "multipath", encoded = true) String multiPath,
@Query("after") String after, @HeaderMap Map<String, String> headers); @Query("after") String after, @HeaderMap Map<String, String> headers);
@POST("r/{subredditName}/api/delete_sr_icon")
Call<String> deleteSrIcon(@HeaderMap Map<String, String> headers, @Path("subredditName") String subredditName);
@POST("r/{subredditName}/api/delete_sr_banner")
Call<String> deleteSrBanner(@HeaderMap Map<String, String> headers, @Path("subredditName") String subredditName);
@Multipart
@POST("r/{subredditName}/api/upload_sr_img")
Call<String> uploadSrImg(@HeaderMap Map<String, String> headers,
@Path("subredditName") String subredditName,
@PartMap Map<String, RequestBody> params,
@Part MultipartBody.Part file);
@GET("r/{subredditName}/about/edit?raw_json=1")
Call<String> getSubredditSetting(@HeaderMap Map<String, String> headers, @Path("subredditName") String subredditName);
@FormUrlEncoded
@POST("/api/site_admin")
Call<String> postSiteAdmin(@HeaderMap Map<String, String> headers, @FieldMap Map<String, String> params);
} }

View File

@ -0,0 +1,11 @@
package ml.docilealligator.infinityforreddit.events;
public class SubmitChangeAvatarEvent {
public final boolean isSuccess;
public final String errorMessage;
public SubmitChangeAvatarEvent(boolean isSuccess, String errorMessage) {
this.isSuccess = isSuccess;
this.errorMessage = errorMessage;
}
}

View File

@ -0,0 +1,11 @@
package ml.docilealligator.infinityforreddit.events;
public class SubmitChangeBannerEvent {
public final boolean isSuccess;
public final String errorMessage;
public SubmitChangeBannerEvent(boolean isSuccess, String errorMessage) {
this.isSuccess = isSuccess;
this.errorMessage = errorMessage;
}
}

View File

@ -0,0 +1,11 @@
package ml.docilealligator.infinityforreddit.events;
public class SubmitSaveProfileEvent {
public final boolean isSuccess;
public final String errorMessage;
public SubmitSaveProfileEvent(boolean isSuccess, String errorMessage) {
this.isSuccess = isSuccess;
this.errorMessage = errorMessage;
}
}

View File

@ -0,0 +1,260 @@
package ml.docilealligator.infinityforreddit.services;
import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.bumptech.glide.Glide;
import jp.wasabeef.glide.transformations.CropTransformation;
import ml.docilealligator.infinityforreddit.Infinity;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
import ml.docilealligator.infinityforreddit.events.SubmitChangeAvatarEvent;
import ml.docilealligator.infinityforreddit.events.SubmitChangeBannerEvent;
import ml.docilealligator.infinityforreddit.events.SubmitSaveProfileEvent;
import ml.docilealligator.infinityforreddit.utils.EditProfileUtils;
import ml.docilealligator.infinityforreddit.utils.NotificationUtils;
import org.greenrobot.eventbus.EventBus;
import retrofit2.Retrofit;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.FileNotFoundException;
import java.util.Random;
import java.util.concurrent.ExecutionException;
public class EditProfileService extends Service {
public static final String EXTRA_ACCESS_TOKEN = "EAT";
public static final String EXTRA_ACCOUNT_NAME = "EAN";
public static final String EXTRA_DISPLAY_NAME = "EDN";
public static final String EXTRA_ABOUT_YOU = "EAY";
public static final String EXTRA_POST_TYPE = "EPT";
public static final int EXTRA_POST_TYPE_UNKNOWN = 0x500;
public static final int EXTRA_POST_TYPE_CHANGE_BANNER = 0x501;
public static final int EXTRA_POST_TYPE_CHANGE_AVATAR = 0x502;
public static final int EXTRA_POST_TYPE_SAVE_EDIT_PROFILE = 0x503;
private static final String EXTRA_MEDIA_URI = "EU";
private static final int MAX_BANNER_WIDTH = 1280;
private static final int MIN_BANNER_WIDTH = 640;
private static final int AVATAR_SIZE = 256;
@Inject
@Named("oauth")
Retrofit mOauthRetrofit;
@Inject
CustomThemeWrapper mCustomThemeWrapper;
private Handler handler;
private ServiceHandler serviceHandler;
@Override
public void onCreate() {
super.onCreate();
((Infinity) getApplication()).getAppComponent().inject(this);
handler = new Handler();
HandlerThread thread = new HandlerThread("ServiceStartArguments",
Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
serviceHandler = new ServiceHandler(thread.getLooper());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
((Infinity) getApplication()).getAppComponent().inject(this);
NotificationChannelCompat serviceChannel =
new NotificationChannelCompat.Builder(
NotificationUtils.CHANNEL_SUBMIT_POST,
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(NotificationUtils.CHANNEL_SUBMIT_POST)
.build();
NotificationManagerCompat manager = NotificationManagerCompat.from(this);
manager.createNotificationChannel(serviceChannel);
int randomNotificationIdOffset = new Random().nextInt(10000);
Bundle bundle = intent.getExtras();
final int postType = intent.getIntExtra(EXTRA_POST_TYPE, EXTRA_POST_TYPE_UNKNOWN);
switch (postType) {
case EXTRA_POST_TYPE_CHANGE_BANNER:
bundle.putString(EXTRA_MEDIA_URI, intent.getData().toString());
startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset,
createNotification(R.string.submit_change_banner));
break;
case EXTRA_POST_TYPE_CHANGE_AVATAR:
bundle.putString(EXTRA_MEDIA_URI, intent.getData().toString());
startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset,
createNotification(R.string.submit_change_avatar));
break;
case EXTRA_POST_TYPE_SAVE_EDIT_PROFILE:
startForeground(NotificationUtils.SUBMIT_POST_SERVICE_NOTIFICATION_ID + randomNotificationIdOffset,
createNotification(R.string.submit_save_profile));
break;
default:
case EXTRA_POST_TYPE_UNKNOWN:
break;
}
Message msg = serviceHandler.obtainMessage();
msg.setData(bundle);
serviceHandler.sendMessage(msg);
return START_NOT_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void submitChangeBanner(String accessToken, Uri mediaUri, String accountName) {
try {
final int width = getWidthBanner(mediaUri);
final int height = Math.round(width * 3 / 10f); // ratio 10:3
CropTransformation bannerCrop = new CropTransformation(width, height, CropTransformation.CropType.CENTER);
Bitmap resource = Glide.with(this).asBitmap().skipMemoryCache(true)
.load(mediaUri).transform(bannerCrop).submit().get();
EditProfileUtils.uploadBanner(mOauthRetrofit, accessToken, accountName, resource, new EditProfileUtils.EditProfileUtilsListener() {
@Override
public void success() {
handler.post(() -> EventBus.getDefault().post(new SubmitChangeBannerEvent(true, "")));
stopService();
}
@Override
public void failed(String message) {
handler.post(() -> EventBus.getDefault().post(new SubmitChangeBannerEvent(false, message)));
stopService();
}
});
} catch (InterruptedException | ExecutionException | FileNotFoundException e) {
e.printStackTrace();
stopService();
}
}
private void submitChangeAvatar(String accessToken, Uri mediaUri, String accountName) {
try {
final CropTransformation avatarCrop = new CropTransformation(AVATAR_SIZE, AVATAR_SIZE, CropTransformation.CropType.CENTER);
final Bitmap resource = Glide.with(this).asBitmap().skipMemoryCache(true)
.load(mediaUri).transform(avatarCrop).submit().get();
EditProfileUtils.uploadAvatar(mOauthRetrofit, accessToken, accountName, resource, new EditProfileUtils.EditProfileUtilsListener() {
@Override
public void success() {
handler.post(() -> EventBus.getDefault().post(new SubmitChangeAvatarEvent(true, "")));
stopService();
}
@Override
public void failed(String message) {
handler.post(() -> EventBus.getDefault().post(new SubmitChangeAvatarEvent(false, message)));
stopService();
}
});
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
stopService();
}
}
private void submitSaveEditProfile(String accessToken,
String accountName,
String displayName,
String publicDesc
) {
EditProfileUtils.updateProfile(mOauthRetrofit,
accessToken,
accountName,
displayName,
publicDesc,
new EditProfileUtils.EditProfileUtilsListener() {
@Override
public void success() {
handler.post(() -> EventBus.getDefault().post(new SubmitSaveProfileEvent(true, "")));
stopService();
}
@Override
public void failed(String message) {
handler.post(() -> EventBus.getDefault().post(new SubmitSaveProfileEvent(false, message)));
stopService();
}
});
}
private Notification createNotification(int stringResId) {
return new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_SUBMIT_POST)
.setContentTitle(getString(stringResId))
.setContentText(getString(R.string.please_wait))
.setSmallIcon(R.drawable.ic_notification)
.setColor(mCustomThemeWrapper.getColorPrimaryLightTheme())
.build();
}
private void stopService() {
stopForeground(true);
stopSelf();
}
private int getWidthBanner(Uri mediaUri) throws FileNotFoundException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(getContentResolver().openInputStream(mediaUri), null, options);
return Math.max(Math.min(options.outWidth, MAX_BANNER_WIDTH), MIN_BANNER_WIDTH);
}
private class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
Bundle bundle = msg.getData();
String accessToken = bundle.getString(EXTRA_ACCESS_TOKEN);
String accountName = bundle.getString(EXTRA_ACCOUNT_NAME);
final int postType = bundle.getInt(EXTRA_POST_TYPE, EXTRA_POST_TYPE_UNKNOWN);
switch (postType) {
case EXTRA_POST_TYPE_CHANGE_BANNER:
submitChangeBanner(accessToken,
Uri.parse(bundle.getString(EXTRA_MEDIA_URI)),
accountName);
break;
case EXTRA_POST_TYPE_CHANGE_AVATAR:
submitChangeAvatar(accessToken,
Uri.parse(bundle.getString(EXTRA_MEDIA_URI)),
accountName);
break;
case EXTRA_POST_TYPE_SAVE_EDIT_PROFILE:
submitSaveEditProfile(
accessToken,
accountName,
bundle.getString(EXTRA_DISPLAY_NAME),
bundle.getString(EXTRA_ABOUT_YOU)
);
break;
default:
case EXTRA_POST_TYPE_UNKNOWN:
break;
}
}
}
}

View File

@ -0,0 +1,634 @@
package ml.docilealligator.infinityforreddit.subreddit;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.Objects;
public class SubredditSettingData {
// Content visibility || Posts to this profile can appear in r/all and your profile can be discovered in /users
@SerializedName("default_set")
private boolean defaultSet;
@SerializedName("toxicity_threshold_chat_level")
private int toxicityThresholdChatLevel;
@SerializedName("crowd_control_chat_level")
private int crowdControlChatLevel;
@SerializedName("restrict_posting")
private boolean restrictPosting;
@SerializedName("public_description")
private String publicDescription;
@SerializedName("subreddit_id")
private String subredditId;
@SerializedName("allow_images")
private boolean allowImages;
@SerializedName("free_form_reports")
private boolean freeFormReports;
@SerializedName("domain")
@Nullable
private String domain;
@SerializedName("show_media")
private boolean showMedia;
@SerializedName("wiki_edit_age")
private int wikiEditAge;
@SerializedName("submit_text")
private String submitText;
@SerializedName("allow_polls")
private boolean allowPolls;
@SerializedName("title")
private String title;
@SerializedName("collapse_deleted_comments")
private boolean collapseDeletedComments;
@SerializedName("wikimode")
private String wikiMode;
@SerializedName("should_archive_posts")
private boolean shouldArchivePosts;
@SerializedName("allow_videos")
private boolean allowVideos;
@SerializedName("allow_galleries")
private boolean allowGalleries;
@SerializedName("crowd_control_level")
private int crowdControlLevel;
@SerializedName("crowd_control_mode")
private boolean crowdControlMode;
@SerializedName("welcome_message_enabled")
private boolean welcomeMessageEnabled;
@SerializedName("welcome_message_text")
@Nullable
private String welcomeMessageText;
@SerializedName("over_18")
private boolean over18;
@SerializedName("suggested_comment_sort")
private String suggestedCommentSort;
@SerializedName("disable_contributor_requests")
private boolean disableContributorRequests;
@SerializedName("original_content_tag_enabled")
private boolean originalContentTagEnabled;
@SerializedName("description")
private String description;
@SerializedName("submit_link_label")
private String submitLinkLabel;
@SerializedName("spoilers_enabled")
private boolean spoilersEnabled;
@SerializedName("allow_post_crossposts")
private boolean allowPostCrossPosts;
@SerializedName("spam_comments")
private String spamComments;
@SerializedName("public_traffic")
private boolean publicTraffic;
@SerializedName("restrict_commenting")
private boolean restrictCommenting;
@SerializedName("new_pinned_post_pns_enabled")
private boolean newPinnedPostPnsEnabled;
@SerializedName("submit_text_label")
private String submitTextLabel;
@SerializedName("all_original_content")
private boolean allOriginalContent;
@SerializedName("spam_selfposts")
private String spamSelfPosts;
@SerializedName("key_color")
private String keyColor;
@SerializedName("language")
private String language;
@SerializedName("wiki_edit_karma")
private int wikiEditKarma;
@SerializedName("hide_ads")
private boolean hideAds;
@SerializedName("prediction_leaderboard_entry_type")
private int predictionLeaderboardEntryType;
@SerializedName("header_hover_text")
private String headerHoverText;
@SerializedName("allow_chat_post_creation")
private boolean allowChatPostCreation;
@SerializedName("allow_prediction_contributors")
private boolean allowPredictionContributors;
@SerializedName("allow_discovery")
private boolean allowDiscovery;
@SerializedName("accept_followers")
private boolean acceptFollowers;
@SerializedName("exclude_banned_modqueue")
private boolean excludeBannedModQueue;
@SerializedName("allow_predictions_tournament")
private boolean allowPredictionsTournament;
@SerializedName("show_media_preview")
private boolean showMediaPreview;
@SerializedName("comment_score_hide_mins")
private int commentScoreHideMins;
@SerializedName("subreddit_type")
private String subredditType;
@SerializedName("spam_links")
private String spamLinks;
@SerializedName("allow_predictions")
private boolean allowPredictions;
@SerializedName("user_flair_pns_enabled")
private boolean userFlairPnsEnabled;
@SerializedName("content_options")
private String contentOptions;
public boolean isDefaultSet() {
return defaultSet;
}
public void setDefaultSet(boolean defaultSet) {
this.defaultSet = defaultSet;
}
public int getToxicityThresholdChatLevel() {
return toxicityThresholdChatLevel;
}
public void setToxicityThresholdChatLevel(int toxicityThresholdChatLevel) {
this.toxicityThresholdChatLevel = toxicityThresholdChatLevel;
}
public int getCrowdControlChatLevel() {
return crowdControlChatLevel;
}
public void setCrowdControlChatLevel(int crowdControlChatLevel) {
this.crowdControlChatLevel = crowdControlChatLevel;
}
public boolean isRestrictPosting() {
return restrictPosting;
}
public void setRestrictPosting(boolean restrictPosting) {
this.restrictPosting = restrictPosting;
}
public String getPublicDescription() {
return publicDescription;
}
public void setPublicDescription(String publicDescription) {
this.publicDescription = publicDescription;
}
public String getSubredditId() {
return subredditId;
}
public void setSubredditId(String subredditId) {
this.subredditId = subredditId;
}
public boolean isAllowImages() {
return allowImages;
}
public void setAllowImages(boolean allowImages) {
this.allowImages = allowImages;
}
public boolean isFreeFormReports() {
return freeFormReports;
}
public void setFreeFormReports(boolean freeFormReports) {
this.freeFormReports = freeFormReports;
}
@Nullable
public String getDomain() {
return domain;
}
public void setDomain(@Nullable String domain) {
this.domain = domain;
}
public boolean isShowMedia() {
return showMedia;
}
public void setShowMedia(boolean showMedia) {
this.showMedia = showMedia;
}
public int getWikiEditAge() {
return wikiEditAge;
}
public void setWikiEditAge(int wikiEditAge) {
this.wikiEditAge = wikiEditAge;
}
public String getSubmitText() {
return submitText;
}
public void setSubmitText(String submitText) {
this.submitText = submitText;
}
public boolean isAllowPolls() {
return allowPolls;
}
public void setAllowPolls(boolean allowPolls) {
this.allowPolls = allowPolls;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isCollapseDeletedComments() {
return collapseDeletedComments;
}
public void setCollapseDeletedComments(boolean collapseDeletedComments) {
this.collapseDeletedComments = collapseDeletedComments;
}
public String getWikiMode() {
return wikiMode;
}
public void setWikiMode(String wikiMode) {
this.wikiMode = wikiMode;
}
public boolean isShouldArchivePosts() {
return shouldArchivePosts;
}
public void setShouldArchivePosts(boolean shouldArchivePosts) {
this.shouldArchivePosts = shouldArchivePosts;
}
public boolean isAllowVideos() {
return allowVideos;
}
public void setAllowVideos(boolean allowVideos) {
this.allowVideos = allowVideos;
}
public boolean isAllowGalleries() {
return allowGalleries;
}
public void setAllowGalleries(boolean allowGalleries) {
this.allowGalleries = allowGalleries;
}
public int getCrowdControlLevel() {
return crowdControlLevel;
}
public void setCrowdControlLevel(int crowdControlLevel) {
this.crowdControlLevel = crowdControlLevel;
}
public boolean isCrowdControlMode() {
return crowdControlMode;
}
public void setCrowdControlMode(boolean crowdControlMode) {
this.crowdControlMode = crowdControlMode;
}
public boolean isWelcomeMessageEnabled() {
return welcomeMessageEnabled;
}
public void setWelcomeMessageEnabled(boolean welcomeMessageEnabled) {
this.welcomeMessageEnabled = welcomeMessageEnabled;
}
@Nullable
public String getWelcomeMessageText() {
return welcomeMessageText;
}
public void setWelcomeMessageText(@Nullable String welcomeMessageText) {
this.welcomeMessageText = welcomeMessageText;
}
public boolean isOver18() {
return over18;
}
public void setOver18(boolean over18) {
this.over18 = over18;
}
public String getSuggestedCommentSort() {
return suggestedCommentSort;
}
public void setSuggestedCommentSort(String suggestedCommentSort) {
this.suggestedCommentSort = suggestedCommentSort;
}
public boolean isDisableContributorRequests() {
return disableContributorRequests;
}
public void setDisableContributorRequests(boolean disableContributorRequests) {
this.disableContributorRequests = disableContributorRequests;
}
public boolean isOriginalContentTagEnabled() {
return originalContentTagEnabled;
}
public void setOriginalContentTagEnabled(boolean originalContentTagEnabled) {
this.originalContentTagEnabled = originalContentTagEnabled;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getSubmitLinkLabel() {
return submitLinkLabel;
}
public void setSubmitLinkLabel(String submitLinkLabel) {
this.submitLinkLabel = submitLinkLabel;
}
public boolean isSpoilersEnabled() {
return spoilersEnabled;
}
public void setSpoilersEnabled(boolean spoilersEnabled) {
this.spoilersEnabled = spoilersEnabled;
}
public boolean isAllowPostCrossPosts() {
return allowPostCrossPosts;
}
public void setAllowPostCrossPosts(boolean allowPostCrossPosts) {
this.allowPostCrossPosts = allowPostCrossPosts;
}
public String getSpamComments() {
return spamComments;
}
public void setSpamComments(String spamComments) {
this.spamComments = spamComments;
}
public boolean isPublicTraffic() {
return publicTraffic;
}
public void setPublicTraffic(boolean publicTraffic) {
this.publicTraffic = publicTraffic;
}
public boolean isRestrictCommenting() {
return restrictCommenting;
}
public void setRestrictCommenting(boolean restrictCommenting) {
this.restrictCommenting = restrictCommenting;
}
public boolean isNewPinnedPostPnsEnabled() {
return newPinnedPostPnsEnabled;
}
public void setNewPinnedPostPnsEnabled(boolean newPinnedPostPnsEnabled) {
this.newPinnedPostPnsEnabled = newPinnedPostPnsEnabled;
}
public String getSubmitTextLabel() {
return submitTextLabel;
}
public void setSubmitTextLabel(String submitTextLabel) {
this.submitTextLabel = submitTextLabel;
}
public boolean isAllOriginalContent() {
return allOriginalContent;
}
public void setAllOriginalContent(boolean allOriginalContent) {
this.allOriginalContent = allOriginalContent;
}
public String getSpamSelfPosts() {
return spamSelfPosts;
}
public void setSpamSelfPosts(String spamSelfPosts) {
this.spamSelfPosts = spamSelfPosts;
}
public String getKeyColor() {
return keyColor;
}
public void setKeyColor(String keyColor) {
this.keyColor = keyColor;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public int getWikiEditKarma() {
return wikiEditKarma;
}
public void setWikiEditKarma(int wikiEditKarma) {
this.wikiEditKarma = wikiEditKarma;
}
public boolean isHideAds() {
return hideAds;
}
public void setHideAds(boolean hideAds) {
this.hideAds = hideAds;
}
public int getPredictionLeaderboardEntryType() {
return predictionLeaderboardEntryType;
}
public void setPredictionLeaderboardEntryType(int predictionLeaderboardEntryType) {
this.predictionLeaderboardEntryType = predictionLeaderboardEntryType;
}
public String getHeaderHoverText() {
return headerHoverText;
}
public void setHeaderHoverText(String headerHoverText) {
this.headerHoverText = headerHoverText;
}
public boolean isAllowChatPostCreation() {
return allowChatPostCreation;
}
public void setAllowChatPostCreation(boolean allowChatPostCreation) {
this.allowChatPostCreation = allowChatPostCreation;
}
public boolean isAllowPredictionContributors() {
return allowPredictionContributors;
}
public void setAllowPredictionContributors(boolean allowPredictionContributors) {
this.allowPredictionContributors = allowPredictionContributors;
}
public boolean isAllowDiscovery() {
return allowDiscovery;
}
public void setAllowDiscovery(boolean allowDiscovery) {
this.allowDiscovery = allowDiscovery;
}
public boolean isAcceptFollowers() {
return acceptFollowers;
}
public void setAcceptFollowers(boolean acceptFollowers) {
this.acceptFollowers = acceptFollowers;
}
public boolean isExcludeBannedModQueue() {
return excludeBannedModQueue;
}
public void setExcludeBannedModQueue(boolean excludeBannedModQueue) {
this.excludeBannedModQueue = excludeBannedModQueue;
}
public boolean isAllowPredictionsTournament() {
return allowPredictionsTournament;
}
public void setAllowPredictionsTournament(boolean allowPredictionsTournament) {
this.allowPredictionsTournament = allowPredictionsTournament;
}
public boolean isShowMediaPreview() {
return showMediaPreview;
}
public void setShowMediaPreview(boolean showMediaPreview) {
this.showMediaPreview = showMediaPreview;
}
public int getCommentScoreHideMins() {
return commentScoreHideMins;
}
public void setCommentScoreHideMins(int commentScoreHideMins) {
this.commentScoreHideMins = commentScoreHideMins;
}
public String getSubredditType() {
return subredditType;
}
public void setSubredditType(String subredditType) {
this.subredditType = subredditType;
}
public String getSpamLinks() {
return spamLinks;
}
public void setSpamLinks(String spamLinks) {
this.spamLinks = spamLinks;
}
public boolean isAllowPredictions() {
return allowPredictions;
}
public void setAllowPredictions(boolean allowPredictions) {
this.allowPredictions = allowPredictions;
}
public boolean isUserFlairPnsEnabled() {
return userFlairPnsEnabled;
}
public void setUserFlairPnsEnabled(boolean userFlairPnsEnabled) {
this.userFlairPnsEnabled = userFlairPnsEnabled;
}
public String getContentOptions() {
return contentOptions;
}
public void setContentOptions(String contentOptions) {
this.contentOptions = contentOptions;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SubredditSettingData that = (SubredditSettingData) o;
return defaultSet == that.defaultSet && toxicityThresholdChatLevel == that.toxicityThresholdChatLevel
&& crowdControlChatLevel == that.crowdControlChatLevel && restrictPosting == that.restrictPosting
&& allowImages == that.allowImages && freeFormReports == that.freeFormReports && showMedia == that.showMedia
&& wikiEditAge == that.wikiEditAge && allowPolls == that.allowPolls && collapseDeletedComments == that.collapseDeletedComments
&& shouldArchivePosts == that.shouldArchivePosts && allowVideos == that.allowVideos
&& allowGalleries == that.allowGalleries && crowdControlLevel == that.crowdControlLevel
&& crowdControlMode == that.crowdControlMode && welcomeMessageEnabled == that.welcomeMessageEnabled
&& over18 == that.over18 && disableContributorRequests == that.disableContributorRequests
&& originalContentTagEnabled == that.originalContentTagEnabled && spoilersEnabled == that.spoilersEnabled
&& allowPostCrossPosts == that.allowPostCrossPosts && publicTraffic == that.publicTraffic
&& restrictCommenting == that.restrictCommenting && newPinnedPostPnsEnabled == that.newPinnedPostPnsEnabled
&& allOriginalContent == that.allOriginalContent && wikiEditKarma == that.wikiEditKarma
&& hideAds == that.hideAds && predictionLeaderboardEntryType == that.predictionLeaderboardEntryType
&& allowChatPostCreation == that.allowChatPostCreation && allowPredictionContributors == that.allowPredictionContributors
&& allowDiscovery == that.allowDiscovery && acceptFollowers == that.acceptFollowers
&& excludeBannedModQueue == that.excludeBannedModQueue && allowPredictionsTournament == that.allowPredictionsTournament
&& showMediaPreview == that.showMediaPreview && commentScoreHideMins == that.commentScoreHideMins
&& allowPredictions == that.allowPredictions && userFlairPnsEnabled == that.userFlairPnsEnabled
&& Objects.equals(publicDescription, that.publicDescription) && Objects.equals(subredditId, that.subredditId)
&& Objects.equals(domain, that.domain) && Objects.equals(submitText, that.submitText)
&& Objects.equals(title, that.title) && Objects.equals(wikiMode, that.wikiMode) &&
Objects.equals(welcomeMessageText, that.welcomeMessageText) && Objects.equals(suggestedCommentSort, that.suggestedCommentSort)
&& Objects.equals(description, that.description) && Objects.equals(submitLinkLabel, that.submitLinkLabel)
&& Objects.equals(spamComments, that.spamComments) && Objects.equals(submitTextLabel, that.submitTextLabel)
&& Objects.equals(spamSelfPosts, that.spamSelfPosts) && Objects.equals(keyColor, that.keyColor)
&& Objects.equals(language, that.language) && Objects.equals(headerHoverText, that.headerHoverText)
&& Objects.equals(subredditType, that.subredditType) && Objects.equals(spamLinks, that.spamLinks)
&& Objects.equals(contentOptions, that.contentOptions);
}
@Override
public int hashCode() {
return Objects.hash(defaultSet, toxicityThresholdChatLevel, crowdControlChatLevel, restrictPosting,
publicDescription, subredditId, allowImages, freeFormReports, domain, showMedia, wikiEditAge,
submitText, allowPolls, title, collapseDeletedComments, wikiMode, shouldArchivePosts,
allowVideos, allowGalleries, crowdControlLevel, crowdControlMode, welcomeMessageEnabled,
welcomeMessageText, over18, suggestedCommentSort, disableContributorRequests, originalContentTagEnabled,
description, submitLinkLabel, spoilersEnabled, allowPostCrossPosts, spamComments, publicTraffic,
restrictCommenting, newPinnedPostPnsEnabled, submitTextLabel, allOriginalContent, spamSelfPosts,
keyColor, language, wikiEditKarma, hideAds, predictionLeaderboardEntryType, headerHoverText,
allowChatPostCreation, allowPredictionContributors, allowDiscovery, acceptFollowers,
excludeBannedModQueue, allowPredictionsTournament, showMediaPreview, commentScoreHideMins,
subredditType, spamLinks, allowPredictions, userFlairPnsEnabled, contentOptions);
}
}

View File

@ -52,9 +52,10 @@ public class ParseUserData {
boolean isFriend = userDataJson.getBoolean(JSONUtils.IS_FRIEND_KEY); boolean isFriend = userDataJson.getBoolean(JSONUtils.IS_FRIEND_KEY);
boolean isNsfw = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getBoolean(JSONUtils.OVER_18_KEY); boolean isNsfw = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getBoolean(JSONUtils.OVER_18_KEY);
String description = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getString(JSONUtils.PUBLIC_DESCRIPTION_KEY); String description = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getString(JSONUtils.PUBLIC_DESCRIPTION_KEY);
String title = userDataJson.getJSONObject(JSONUtils.SUBREDDIT_KEY).getString(JSONUtils.TITLE_KEY);
return new UserData(userName, iconImageUrl, bannerImageUrl, linkKarma, commentKarma, awarderKarma, return new UserData(userName, iconImageUrl, bannerImageUrl, linkKarma, commentKarma, awarderKarma,
awardeeKarma, totalKarma, cakeday, isGold, isFriend, canBeFollowed, isNsfw, description); awardeeKarma, totalKarma, cakeday, isGold, isFriend, canBeFollowed, isNsfw, description, title);
} }
interface ParseUserDataListener { interface ParseUserDataListener {

View File

@ -38,12 +38,14 @@ public class UserData {
private boolean isNSFW; private boolean isNSFW;
@ColumnInfo(name = "description") @ColumnInfo(name = "description")
private String description; private String description;
@ColumnInfo(name = "title")
private String title;
@Ignore @Ignore
private boolean isSelected; private boolean isSelected;
public UserData(@NonNull String name, String iconUrl, String banner, int linkKarma, int commentKarma, public UserData(@NonNull String name, String iconUrl, String banner, int linkKarma, int commentKarma,
int awarderKarma, int awardeeKarma, int totalKarma, long cakeday, boolean isGold, int awarderKarma, int awardeeKarma, int totalKarma, long cakeday, boolean isGold,
boolean isFriend, boolean canBeFollowed, boolean isNSFW, String description) { boolean isFriend, boolean canBeFollowed, boolean isNSFW, String description, String title) {
this.name = name; this.name = name;
this.iconUrl = iconUrl; this.iconUrl = iconUrl;
this.banner = banner; this.banner = banner;
@ -58,6 +60,7 @@ public class UserData {
this.canBeFollowed = canBeFollowed; this.canBeFollowed = canBeFollowed;
this.isNSFW = isNSFW; this.isNSFW = isNSFW;
this.description = description; this.description = description;
this.title = title;
this.isSelected = false; this.isSelected = false;
} }
@ -118,6 +121,10 @@ public class UserData {
return description; return description;
} }
public String getTitle() {
return title;
}
public boolean isSelected() { public boolean isSelected() {
return isSelected; return isSelected;
} }

View File

@ -0,0 +1,276 @@
package ml.docilealligator.infinityforreddit.utils;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import ml.docilealligator.infinityforreddit.apis.RedditAPI;
import ml.docilealligator.infinityforreddit.subreddit.SubredditSettingData;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import org.json.JSONException;
import org.json.JSONObject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Map;
public final class EditProfileUtils {
public static void updateProfile(Retrofit oauthRetrofit,
String accessToken,
String accountName,
String displayName,
String publicDesc,
EditProfileUtilsListener listener) {
final Map<String, String> oauthHeader = APIUtils.getOAuthHeader(accessToken);
final RedditAPI api = oauthRetrofit.create(RedditAPI.class);
final String name = "u_" + accountName;
api.getSubredditSetting(oauthHeader, name).enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
if (response.isSuccessful()) {
try {
final String json = response.body();
if (json == null) {
listener.failed("Something happen.");
return;
}
final JSONObject resBody = new JSONObject(json);
final SubredditSettingData data = new Gson().fromJson(resBody.getString("data"), SubredditSettingData.class);
if (data.getPublicDescription().equals(publicDesc)
&& data.getTitle().equals(displayName)) {
// no-op
listener.success();
return;
}
final Map<String, String> params = new HashMap<>();
params.put("api_type", "json");
params.put("sr", data.getSubredditId());
params.put("name", name);
params.put("type", data.getSubredditType());
// Only this 2 param
params.put("public_description", publicDesc);
params.put("title", displayName);
// Official Reddit app have this 2 params
// 1 = disable; 0 = enable || Active in communities visibility || Show which communities I am active in on my profile.
params.put("toxicity_threshold_chat_level", String.valueOf(data.getToxicityThresholdChatLevel()));
// Content visibility || Posts to this profile can appear in r/all and your profile can be discovered in /users
params.put("default_set", String.valueOf(data.isDefaultSet()));
// Allow people to follow you || Followers will be notified about posts you make to your profile and see them in their home feed.
params.put("accept_followers", String.valueOf(data.isAcceptFollowers()));
params.put("allow_top", String.valueOf(data.isPublicTraffic())); //
params.put("link_type", String.valueOf(data.getContentOptions())); //
//
params.put("original_content_tag_enabled", String.valueOf(data.isOriginalContentTagEnabled()));
params.put("new_pinned_post_pns_enabled", String.valueOf(data.isNewPinnedPostPnsEnabled()));
params.put("prediction_leaderboard_entry_type", String.valueOf(data.getPredictionLeaderboardEntryType()));
params.put("restrict_commenting", String.valueOf(data.isRestrictCommenting()));
params.put("restrict_posting", String.valueOf(data.isRestrictPosting()));
params.put("should_archive_posts", String.valueOf(data.isShouldArchivePosts()));
params.put("show_media", String.valueOf(data.isShowMedia()));
params.put("show_media_preview", String.valueOf(data.isShowMediaPreview()));
params.put("spam_comments", data.getSpamComments());
params.put("spam_links", data.getSpamLinks());
params.put("spam_selfposts", data.getSpamSelfPosts());
params.put("spoilers_enabled", String.valueOf(data.isSpoilersEnabled()));
params.put("submit_link_label", data.getSubmitLinkLabel());
params.put("submit_text", data.getSubmitText());
params.put("submit_text_label", data.getSubmitTextLabel());
params.put("user_flair_pns_enabled", String.valueOf(data.isUserFlairPnsEnabled()));
params.put("all_original_content", String.valueOf(data.isAllOriginalContent()));
params.put("allow_chat_post_creation", String.valueOf(data.isAllowChatPostCreation()));
params.put("allow_discovery", String.valueOf(data.isAllowDiscovery()));
params.put("allow_galleries", String.valueOf(data.isAllowGalleries()));
params.put("allow_images", String.valueOf(data.isAllowImages()));
params.put("allow_polls", String.valueOf(data.isAllowPolls()));
params.put("allow_post_crossposts", String.valueOf(data.isAllowPostCrossPosts()));
params.put("allow_prediction_contributors", String.valueOf(data.isAllowPredictionContributors()));
params.put("allow_predictions", String.valueOf(data.isAllowPredictions()));
params.put("allow_predictions_tournament", String.valueOf(data.isAllowPredictionsTournament()));
params.put("allow_videos", String.valueOf(data.isAllowVideos()));
params.put("collapse_deleted_comments", String.valueOf(data.isCollapseDeletedComments()));
params.put("comment_score_hide_mins", String.valueOf(data.getCommentScoreHideMins()));
params.put("crowd_control_chat_level", String.valueOf(data.getCrowdControlChatLevel()));
params.put("crowd_control_filter", String.valueOf(data.getCrowdControlChatLevel()));
params.put("crowd_control_level", String.valueOf(data.getCrowdControlLevel()));
params.put("crowd_control_mode", String.valueOf(data.isCrowdControlMode()));
params.put("description", data.getDescription());
params.put("disable_contributor_requests", String.valueOf(data.isDisableContributorRequests()));
params.put("exclude_banned_modqueue", String.valueOf(data.isExcludeBannedModQueue()));
params.put("free_form_reports", String.valueOf(data.isFreeFormReports()));
params.put("header-title", data.getHeaderHoverText());
params.put("hide_ads", String.valueOf(data.isHideAds()));
params.put("key_color", data.getKeyColor());
params.put("lang", data.getLanguage());
params.put("over_18", String.valueOf(data.isOver18()));
params.put("suggested_comment_sort", data.getSuggestedCommentSort());
params.put("welcome_message_enabled", String.valueOf(data.isWelcomeMessageEnabled()));
params.put("welcome_message_text", String.valueOf(data.getWelcomeMessageText()));
params.put("wiki_edit_age", String.valueOf(data.getWikiEditAge()));
params.put("wiki_edit_karma", String.valueOf(data.getWikiEditKarma()));
params.put("wikimode", data.getWikiMode());
api.postSiteAdmin(oauthHeader, params)
.enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
if (response.isSuccessful()) listener.success();
else listener.failed(response.message());
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
t.printStackTrace();
listener.failed(t.getLocalizedMessage());
}
});
} catch (JSONException e) {
listener.failed(e.getLocalizedMessage());
}
} else {
listener.failed(response.message());
}
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
t.printStackTrace();
listener.failed(t.getLocalizedMessage());
}
});
}
public static void uploadAvatar(Retrofit oauthRetrofit,
String accessToken,
String accountName,
Bitmap image,
EditProfileUtilsListener listener) {
oauthRetrofit.create(RedditAPI.class)
.uploadSrImg(
APIUtils.getOAuthHeader(accessToken),
"u_" + accountName,
requestBodyUploadSr("icon"),
fileToUpload(image, accountName + "-icon"))
.enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call,
@NonNull Response<String> response) {
if (response.isSuccessful()) listener.success();
else listener.failed(response.message());
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
t.printStackTrace();
listener.failed(t.getLocalizedMessage());
}
});
}
public static void uploadBanner(Retrofit oauthRetrofit,
String accessToken,
String accountName,
Bitmap image,
EditProfileUtilsListener listener) {
oauthRetrofit.create(RedditAPI.class)
.uploadSrImg(
APIUtils.getOAuthHeader(accessToken),
"u_" + accountName,
requestBodyUploadSr("banner"),
fileToUpload(image, accountName + "-banner"))
.enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call,
@NonNull Response<String> response) {
if (response.isSuccessful()) listener.success();
else listener.failed(response.message());
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
t.printStackTrace();
listener.failed(t.getLocalizedMessage());
}
});
}
public static void deleteAvatar(Retrofit oauthRetrofit,
String accessToken,
String accountName,
EditProfileUtilsListener listener) {
oauthRetrofit.create(RedditAPI.class)
.deleteSrIcon(APIUtils.getOAuthHeader(accessToken), "u_" + accountName)
.enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call,
@NonNull Response<String> response) {
if (response.isSuccessful()) listener.success();
else listener.failed(response.message());
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
t.printStackTrace();
listener.failed(t.getLocalizedMessage());
}
});
}
public static void deleteBanner(Retrofit oauthRetrofit,
String accessToken,
String accountName,
EditProfileUtilsListener listener) {
oauthRetrofit.create(RedditAPI.class)
.deleteSrBanner(APIUtils.getOAuthHeader(accessToken), "u_" + accountName)
.enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call,
@NonNull Response<String> response) {
if (response.isSuccessful()) listener.success();
else listener.failed(response.message());
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
t.printStackTrace();
listener.failed(t.getLocalizedMessage());
}
});
}
private static Map<String, RequestBody> requestBodyUploadSr(String type) {
Map<String, RequestBody> param = new HashMap<>();
param.put("upload_type", APIUtils.getRequestBody(type));
param.put("img_type", APIUtils.getRequestBody("jpg"));
return param;
}
private static MultipartBody.Part fileToUpload(Bitmap image, String fileName) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, stream);
byte[] byteArray = stream.toByteArray();
RequestBody fileBody = RequestBody.create(byteArray,
MediaType.parse("image/*"));
return MultipartBody.Part.createFormData("file", fileName + ".jpg", fileBody);
}
public interface EditProfileUtilsListener {
void success();
void failed(String message);
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"
android:tint="?attr/colorOnPrimary">
<size
android:width="24dp"
android:height="24dp" />
<solid android:color="#7F000000" />
</shape>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout_view_edit_profile_activity"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activities.EditProfileActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_layout_view_edit_profile_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_view_edit_profile_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:navigationIcon="?attr/homeAsUpIndicator"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="@string/action_edit_profile" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content_view_edit_profile_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/frame_layout_view_banner_edit_profile_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<ImageView
android:id="@+id/image_view_banner_edit_profile_activity"
android:layout_width="match_parent"
android:layout_height="180dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
tools:src="@tools:sample/backgrounds/scenic" />
<ImageView
android:id="@+id/image_view_change_banner_edit_profile_activity"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end|bottom"
android:layout_margin="4dp"
android:background="@drawable/ic_dot_outline"
android:contentDescription="@null"
android:padding="4dp"
app:srcCompat="@drawable/ic_outline_add_a_photo_day_night_24dp" />
</FrameLayout>
<FrameLayout
android:id="@+id/frame_layout_view_avatar_edit_profile_activity"
android:layout_width="74dp"
android:layout_height="74dp"
android:layout_marginStart="24dp"
android:layout_marginTop="143dp"
android:elevation="4dp">
<ImageView
android:id="@+id/image_view_avatar_edit_profile_activity"
android:layout_width="74dp"
android:layout_height="74dp"
android:contentDescription="@null"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/image_view_change_avatar_edit_profile_activity"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end|bottom"
android:layout_margin="4dp"
android:background="@drawable/ic_dot_outline"
android:contentDescription="@null"
android:padding="4dp"
app:srcCompat="@drawable/ic_outline_add_a_photo_day_night_24dp" />
</FrameLayout>
</FrameLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="8dp"
android:background="?dividerVertical" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="?attr/font_family"
android:text="@string/display_name_text"
android:textSize="?attr/font_18" />
<EditText
android:id="@+id/edit_text_display_name_edit_profile_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:hint="@string/display_name_hint"
android:importantForAutofill="no"
android:inputType="textCapSentences"
android:lines="1"
android:maxLength="30"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/display_name_description" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="?attr/font_family"
android:text="@string/about_you_text"
android:textSize="?attr/font_18" />
<EditText
android:id="@+id/edit_text_about_you_edit_profile_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:hint="@string/about_you_hint"
android:importantForAutofill="no"
android:inputType="textCapSentences|textMultiLine"
android:lines="4"
android:maxLength="200"
android:maxLines="4"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:application="ml.docilealligator.infinityforreddit.activities.EditProfileActivity">
<item
android:id="@+id/action_save_edit_profile_activity"
android:orderInCategory="1"
android:title="@string/action_save"
android:icon="@drawable/ic_save_to_database_24dp"
app:showAsAction="ifRoom" />
</menu>

View File

@ -59,4 +59,9 @@
android:id="@+id/action_block_user_view_user_detail_activity" android:id="@+id/action_block_user_view_user_detail_activity"
android:orderInCategory="10" android:orderInCategory="10"
android:title="@string/action_block_user" /> android:title="@string/action_block_user" />
<item
android:id="@+id/action_edit_profile_view_user_detail_activity"
android:orderInCategory="11"
android:title="@string/action_edit_profile" />
</menu> </menu>

View File

@ -1215,4 +1215,29 @@
<string name="app_lock_timeout_12_hours">12 hours</string> <string name="app_lock_timeout_12_hours">12 hours</string>
<string name="app_lock_timeout_24_hours">24 hours</string> <string name="app_lock_timeout_24_hours">24 hours</string>
<!--EditProfileService Notification-->
<string name="submit_change_avatar">Submit Change Avatar</string>
<string name="submit_change_banner">Submit Change Banner</string>
<string name="submit_save_profile">Submit Save Profile</string>
<!--EditProfileActivity-->
<string name="action_edit_profile">Edit Profile</string>
<string name="remove_avatar">Remove Avatar</string>
<string name="remove_banner">Remove Banner</string>
<string name="display_name_text">Display Name</string>
<string name="display_name_hint">Show on your profile page</string>
<string name="display_name_description">This will be displayed to viewer of your profile page and does not change your username</string>
<string name="about_you_text">About You</string>
<string name="about_you_hint">A little description of your self</string>
<string name="message_remove_avatar_success">Success remove Avatar</string>
<string name="message_remove_avatar_failed_fmt">Failed remove Avatar %s</string>
<string name="message_remove_banner_success">Success remove Banner</string>
<string name="message_remove_banner_failed_fmt">Failed remove Banner %s</string>
<string name="message_change_avatar_success">Success changing Avatar</string>
<string name="message_change_avatar_failed_fmt">Failed changing Avatar %s</string>
<string name="message_change_banner_success">Success changing Banner</string>
<string name="message_change_banner_failed_fmt">Failed changing Banner %s</string>
<string name="message_save_profile_success">Success save profile</string>
<string name="message_save_profile_failed_fmt">Failed save profile %s</string>
</resources> </resources>