session-android/src/org/thoughtcrime/securesms/CreateProfileActivity.java

499 lines
19 KiB
Java
Raw Normal View History

2017-08-08 23:37:15 +00:00
package org.thoughtcrime.securesms;
2017-11-25 06:00:30 +00:00
import android.Manifest;
import android.animation.Animator;
2017-11-14 18:22:54 +00:00
import android.annotation.SuppressLint;
2017-08-08 23:37:15 +00:00
import android.app.Activity;
import android.content.Context;
2017-08-08 23:37:15 +00:00
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
2017-08-08 23:37:15 +00:00
import android.os.Bundle;
2017-11-25 06:00:30 +00:00
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.text.Editable;
2017-08-08 23:37:15 +00:00
import android.text.TextUtils;
import android.text.TextWatcher;
2017-08-08 23:37:15 +00:00
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewAnimationUtils;
2017-08-08 23:37:15 +00:00
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
2017-08-08 23:37:15 +00:00
import android.widget.Toast;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.dd.CircularProgressButton;
2017-08-08 23:37:15 +00:00
2019-06-05 06:20:11 +00:00
import org.thoughtcrime.securesms.avatar.AvatarSelection;
2017-08-08 23:37:15 +00:00
import org.thoughtcrime.securesms.components.InputAwareLayout;
2019-06-05 06:20:11 +00:00
import org.thoughtcrime.securesms.components.LabeledEditText;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
2017-08-08 23:37:15 +00:00
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.Address;
2019-10-06 23:06:19 +00:00
import org.thoughtcrime.securesms.database.DatabaseFactory;
2017-08-08 23:37:15 +00:00
import org.thoughtcrime.securesms.dependencies.InjectableType;
2019-06-05 06:20:11 +00:00
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
2017-11-25 06:00:30 +00:00
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
2017-08-08 23:37:15 +00:00
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage;
2019-02-13 19:52:55 +00:00
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
2017-08-08 23:37:15 +00:00
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
2017-08-08 23:37:15 +00:00
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.loki.api.LokiDotNetAPI;
2020-07-15 02:24:43 +00:00
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI;
2017-08-08 23:37:15 +00:00
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
2020-02-24 03:57:51 +00:00
import java.util.Date;
import java.util.Set;
2017-08-08 23:37:15 +00:00
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
2017-08-08 23:37:15 +00:00
import javax.inject.Inject;
2020-02-24 03:57:51 +00:00
import kotlin.Unit;
2019-07-24 02:30:23 +00:00
import network.loki.messenger.R;
2017-11-14 18:22:54 +00:00
@SuppressLint("StaticFieldLeak")
public class CreateProfileActivity extends BaseActionBarActivity implements InjectableType {
2017-08-08 23:37:15 +00:00
private static final String TAG = CreateProfileActivity.class.getSimpleName();
public static final String NEXT_INTENT = "next_intent";
public static final String EXCLUDE_SYSTEM = "exclude_system";
2019-02-13 19:52:55 +00:00
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
2017-08-08 23:37:15 +00:00
@Inject SignalServiceAccountManager accountManager;
private InputAwareLayout container;
private ImageView avatar;
private CircularProgressButton finishButton;
2019-02-13 19:52:55 +00:00
private LabeledEditText name;
private EmojiToggle emojiToggle;
private MediaKeyboard mediaKeyboard;
private View reveal;
2017-08-08 23:37:15 +00:00
private Intent nextIntent;
private byte[] originalAvatarBytes;
2017-08-08 23:37:15 +00:00
private byte[] avatarBytes;
private File captureFile;
2017-08-08 23:37:15 +00:00
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
2017-08-08 23:37:15 +00:00
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
2017-08-08 23:37:15 +00:00
setContentView(R.layout.profile_create_activity);
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
initializeResources();
initializeEmojiInput();
initializeProfileName(getIntent().getBooleanExtra(EXCLUDE_SYSTEM, false));
initializeProfileAvatar(getIntent().getBooleanExtra(EXCLUDE_SYSTEM, false));
2017-08-08 23:37:15 +00:00
ApplicationContext.getInstance(this).injectDependencies(this);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
}
2017-08-08 23:37:15 +00:00
@Override
public void onBackPressed() {
2019-02-13 19:52:55 +00:00
if (container.isInputOpen()) container.hideCurrentInput(name.getInput());
2017-08-08 23:37:15 +00:00
else super.onBackPressed();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (container.getCurrentInput() == mediaKeyboard) {
2017-08-08 23:37:15 +00:00
container.hideAttachedInput(true);
}
}
2017-11-25 06:00:30 +00:00
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
2017-08-08 23:37:15 +00:00
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
2019-03-18 19:09:57 +00:00
case AvatarSelection.REQUEST_CODE_AVATAR:
if (resultCode == Activity.RESULT_OK) {
2017-08-08 23:37:15 +00:00
Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped"));
Uri inputFile = (data != null ? data.getData() : null);
if (inputFile == null && captureFile != null) {
inputFile = Uri.fromFile(captureFile);
}
if (data != null && data.getBooleanExtra("delete", false)) {
avatarBytes = null;
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(this, getResources().getColor(R.color.grey_400)));
} else {
2019-03-18 19:09:57 +00:00
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
}
2017-08-08 23:37:15 +00:00
}
break;
2019-03-18 19:09:57 +00:00
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
2017-08-08 23:37:15 +00:00
if (resultCode == Activity.RESULT_OK) {
new AsyncTask<Void, Void, byte[]>() {
@Override
protected byte[] doInBackground(Void... params) {
try {
2019-03-18 19:09:57 +00:00
BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(CreateProfileActivity.this, AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
return result.getBitmap();
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(byte[] result) {
if (result != null) {
avatarBytes = result;
GlideApp.with(CreateProfileActivity.this)
.load(avatarBytes)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(avatar);
} else {
Toast.makeText(CreateProfileActivity.this, R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
}
}
}.execute();
2017-08-08 23:37:15 +00:00
}
break;
}
}
private void initializeResources() {
TextView skipButton = ViewUtil.findById(this, R.id.skip_button);
2017-08-08 23:37:15 +00:00
this.avatar = ViewUtil.findById(this, R.id.avatar);
this.name = ViewUtil.findById(this, R.id.name);
this.emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle);
this.mediaKeyboard = ViewUtil.findById(this, R.id.emoji_drawer);
2017-08-08 23:37:15 +00:00
this.container = ViewUtil.findById(this, R.id.container);
this.finishButton = ViewUtil.findById(this, R.id.finish_button);
this.reveal = ViewUtil.findById(this, R.id.reveal);
this.nextIntent = getIntent().getParcelableExtra(NEXT_INTENT);
2017-08-08 23:37:15 +00:00
2017-11-25 06:00:30 +00:00
this.avatar.setOnClickListener(view -> Permissions.with(this)
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
2019-03-18 19:09:57 +00:00
.onAnyResult(this::startAvatarSelection)
2017-11-25 06:00:30 +00:00
.execute());
2017-08-08 23:37:15 +00:00
2019-02-13 19:52:55 +00:00
this.name.getInput().addTextChangedListener(new TextWatcher() {
@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) {
Pattern pattern = Pattern.compile("[a-zA-Z0-9_]+");
Matcher matcher = pattern.matcher(s.toString());
if (s.toString().isEmpty()) {
name.getInput().setError("Invalid");
finishButton.setEnabled(false);
} else if (!matcher.matches()) {
name.getInput().setError("Invalid (a-z, A-Z, 0-9 and _ only)");
finishButton.setEnabled(false);
} else if (s.toString().getBytes().length > ProfileCipher.NAME_PADDED_LENGTH) {
2019-02-13 19:52:55 +00:00
name.getInput().setError(getString(R.string.CreateProfileActivity_too_long));
finishButton.setEnabled(false);
2019-02-13 19:52:55 +00:00
} else if (name.getInput().getError() != null || !finishButton.isEnabled()) {
name.getInput().setError(null);
finishButton.setEnabled(true);
}
}
});
2017-08-08 23:37:15 +00:00
this.finishButton.setOnClickListener(view -> {
this.finishButton.setIndeterminateProgressMode(true);
this.finishButton.setProgress(50);
2017-08-08 23:37:15 +00:00
handleUpload();
});
skipButton.setOnClickListener(view -> {
if (nextIntent != null) startActivity(nextIntent);
finish();
});
2017-08-08 23:37:15 +00:00
}
private void initializeProfileName(boolean excludeSystem) {
if (!TextUtils.isEmpty(TextSecurePreferences.getProfileName(this))) {
String profileName = TextSecurePreferences.getProfileName(this);
name.setText(profileName);
2019-02-13 19:52:55 +00:00
name.getInput().setSelection(profileName.length(), profileName.length());
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileName(this).addListener(new ListenableFuture.Listener<String>() {
@Override
public void onSuccess(String result) {
if (!TextUtils.isEmpty(result)) {
name.setText(result);
2019-02-13 19:52:55 +00:00
name.getInput().setSelection(result.length(), result.length());
}
2017-08-08 23:37:15 +00:00
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
}
});
}
}
2017-08-08 23:37:15 +00:00
private void initializeProfileAvatar(boolean excludeSystem) {
Address ourAddress = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this));
if (AvatarHelper.getAvatarFile(this, ourAddress).exists() && AvatarHelper.getAvatarFile(this, ourAddress).length() > 0) {
new AsyncTask<Void, Void, byte[]>() {
@Override
protected byte[] doInBackground(Void... params) {
try {
return Util.readFully(AvatarHelper.getInputStreamFor(CreateProfileActivity.this, ourAddress));
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
2017-08-08 23:37:15 +00:00
}
@Override
protected void onPostExecute(byte[] result) {
if (result != null) {
originalAvatarBytes = result;
avatarBytes = result;
GlideApp.with(CreateProfileActivity.this)
.load(result)
.circleCrop()
.into(avatar);
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileAvatar(this, new ProfileMediaConstraints()).addListener(new ListenableFuture.Listener<byte[]>() {
@Override
public void onSuccess(byte[] result) {
if (result != null) {
originalAvatarBytes = result;
avatarBytes = result;
GlideApp.with(CreateProfileActivity.this)
.load(result)
.circleCrop()
.into(avatar);
}
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
}
});
}
2017-08-08 23:37:15 +00:00
}
private void initializeEmojiInput() {
this.emojiToggle.attach(mediaKeyboard);
2017-08-08 23:37:15 +00:00
this.emojiToggle.setOnClickListener(v -> {
if (container.getCurrentInput() == mediaKeyboard) {
2019-02-13 19:52:55 +00:00
container.showSoftkey(name.getInput());
2017-08-08 23:37:15 +00:00
} else {
container.show(name.getInput(), mediaKeyboard);
2017-08-08 23:37:15 +00:00
}
});
this.mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, new EmojiKeyboardProvider.EmojiEventListener() {
2017-08-08 23:37:15 +00:00
@Override
public void onKeyEvent(KeyEvent keyEvent) {
name.dispatchKeyEvent(keyEvent);
}
@Override
public void onEmojiSelected(String emoji) {
2019-02-13 19:52:55 +00:00
final int start = name.getInput().getSelectionStart();
final int end = name.getInput().getSelectionEnd();
2017-08-08 23:37:15 +00:00
name.getText().replace(Math.min(start, end), Math.max(start, end), emoji);
2019-02-13 19:52:55 +00:00
name.getInput().setSelection(start + emoji.length());
2017-08-08 23:37:15 +00:00
}
}));
2017-08-08 23:37:15 +00:00
this.container.addOnKeyboardShownListener(() -> emojiToggle.setToMedia());
2019-02-13 19:52:55 +00:00
this.name.setOnClickListener(v -> container.showSoftkey(name.getInput()));
2017-08-08 23:37:15 +00:00
}
2019-03-18 19:09:57 +00:00
private void startAvatarSelection() {
captureFile = AvatarSelection.startAvatarSelection(this, avatarBytes != null, true);
2017-11-25 06:00:30 +00:00
}
2017-08-08 23:37:15 +00:00
private void handleUpload() {
final String name;
final StreamDetails avatar;
if (TextUtils.isEmpty(this.name.getText().toString())) name = null;
else name = this.name.getText().toString();
if (avatarBytes == null || avatarBytes.length == 0) avatar = null;
else avatar = new StreamDetails(new ByteArrayInputStream(avatarBytes),
"image/jpeg", avatarBytes.length);
new AsyncTask<Void, Void, Boolean>() {
2017-08-08 23:37:15 +00:00
@Override
protected Boolean doInBackground(Void... params) {
Context context = CreateProfileActivity.this;
2017-08-08 23:37:15 +00:00
2019-06-05 06:20:11 +00:00
TextSecurePreferences.setProfileName(context, name);
2020-07-15 02:24:43 +00:00
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(context).getPublicChatAPI();
2019-10-15 03:32:23 +00:00
if (publicChatAPI != null) {
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChatServers();
for (String server : servers) {
2019-10-15 03:32:23 +00:00
publicChatAPI.setDisplayName(name, server);
}
}
2019-10-06 23:06:19 +00:00
// Loki - Only update avatar if there was a change
if (!Arrays.equals(originalAvatarBytes, avatarBytes)) {
try {
// Loki - Original profile photo code
// ========
// accountManager.setProfileAvatar(profileKey, avatar);
// ========
// Try upload photo with a new profile key
String newProfileKey = ProfileKeyUtil.generateEncodedProfileKey(context);
byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(newProfileKey);
2020-05-22 05:03:15 +00:00
// Loki - Upload the profile photo here
if (avatar != null) {
Log.d("Loki", "Start uploading profile photo");
2020-07-15 02:24:43 +00:00
FileServerAPI storageAPI = FileServerAPI.shared;
2020-02-24 03:57:51 +00:00
LokiDotNetAPI.UploadResult result = storageAPI.uploadProfilePicture(storageAPI.getServer(), profileKey, avatar, () -> {
TextSecurePreferences.setLastProfilePictureUpload(CreateProfileActivity.this, new Date().getTime());
return Unit.INSTANCE;
});
Log.d("Loki", "Profile photo uploaded, the url is " + result.getUrl());
2020-05-11 06:19:26 +00:00
TextSecurePreferences.setProfilePictureURL(context, result.getUrl());
} else {
2020-05-11 06:19:26 +00:00
TextSecurePreferences.setProfilePictureURL(context, null);
}
AvatarHelper.setAvatar(context, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), avatarBytes);
TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
// Upload was successful with this new profile key, we should set it so the other users know to re-fetch profiles
ProfileKeyUtil.setEncodedProfileKey(context, newProfileKey);
// Update profile key on the public chat server
2020-05-13 23:35:34 +00:00
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded();
} catch (Exception e) {
Log.d("Loki", "Failed to upload profile photo: " + e);
return false;
}
2017-08-08 23:37:15 +00:00
}
2019-11-27 22:25:00 +00:00
// ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
2017-08-08 23:37:15 +00:00
return true;
}
@Override
public void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result) {
if (captureFile != null) captureFile.delete();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
else handleFinishedLegacy();
} else {
Toast.makeText(CreateProfileActivity.this, R.string.CreateProfileActivity_problem_setting_profile, Toast.LENGTH_LONG).show();
}
2017-08-08 23:37:15 +00:00
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
2017-08-08 23:37:15 +00:00
}
private void handleFinishedLegacy() {
finishButton.setProgress(0);
if (nextIntent != null) startActivity(nextIntent);
finish();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void handleFinishedLollipop() {
int[] finishButtonLocation = new int[2];
int[] revealLocation = new int[2];
finishButton.getLocationInWindow(finishButtonLocation);
reveal.getLocationInWindow(revealLocation);
int finishX = finishButtonLocation[0] - revealLocation[0];
int finishY = finishButtonLocation[1] - revealLocation[1];
finishX += finishButton.getWidth() / 2;
finishY += finishButton.getHeight() / 2;
Animator animation = ViewAnimationUtils.createCircularReveal(reveal, finishX, finishY, 0f, (float) Math.max(reveal.getWidth(), reveal.getHeight()));
animation.setDuration(500);
animation.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
finishButton.setProgress(0);
if (nextIntent != null) startActivity(nextIntent);
finish();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
reveal.setVisibility(View.VISIBLE);
animation.start();
}
2017-08-08 23:37:15 +00:00
}