1package eu.siacs.conversations.ui;
2
3import android.content.Intent;
4import android.graphics.Bitmap;
5import android.net.Uri;
6import android.os.Bundle;
7import android.util.Log;
8import android.view.Menu;
9import android.view.MenuItem;
10import android.view.View;
11import android.view.View.OnLongClickListener;
12import android.widget.Toast;
13import androidx.activity.result.ActivityResultLauncher;
14import androidx.annotation.NonNull;
15import androidx.annotation.StringRes;
16import androidx.databinding.DataBindingUtil;
17import com.canhub.cropper.CropImageContract;
18import com.canhub.cropper.CropImageContractOptions;
19import com.canhub.cropper.CropImageOptions;
20import com.google.android.material.dialog.MaterialAlertDialogBuilder;
21import eu.siacs.conversations.Config;
22import eu.siacs.conversations.R;
23import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
24import eu.siacs.conversations.entities.Account;
25import eu.siacs.conversations.services.XmppConnectionService;
26import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
27import eu.siacs.conversations.utils.PhoneHelper;
28
29public class PublishProfilePictureActivity extends XmppActivity
30 implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
31
32 public static final int REQUEST_CHOOSE_PICTURE = 0x1337;
33
34 private ActivityPublishProfilePictureBinding binding;
35 private Uri avatarUri;
36 private Uri defaultUri;
37 private Account account;
38 private boolean support = false;
39 private boolean publishing = false;
40 private final OnLongClickListener backToDefaultListener =
41 new OnLongClickListener() {
42
43 @Override
44 public boolean onLongClick(View v) {
45 avatarUri = defaultUri;
46 loadImageIntoPreview(defaultUri);
47 return true;
48 }
49 };
50 private boolean mInitialAccountSetup;
51
52 final ActivityResultLauncher<CropImageContractOptions> cropImage =
53 registerForActivityResult(
54 new CropImageContract(),
55 cropResult -> {
56 if (cropResult.isSuccessful()) {
57 onAvatarPicked(cropResult.getUriContent());
58 }
59 });
60
61 @Override
62 public void onAvatarPublicationSucceeded() {
63 runOnUiThread(
64 () -> {
65 if (mInitialAccountSetup) {
66 Intent intent =
67 new Intent(
68 getApplicationContext(), StartConversationActivity.class);
69 StartConversationActivity.addInviteUri(intent, getIntent());
70 intent.putExtra("init", true);
71 intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
72 startActivity(intent);
73 }
74 Toast.makeText(
75 PublishProfilePictureActivity.this,
76 R.string.avatar_has_been_published,
77 Toast.LENGTH_SHORT)
78 .show();
79 finish();
80 });
81 }
82
83 @Override
84 public void onAvatarPublicationFailed(final int res) {
85 runOnUiThread(
86 () -> {
87 this.binding.hintOrWarning.setText(res);
88 this.binding.hintOrWarning.setVisibility(View.VISIBLE);
89 this.publishing = false;
90 togglePublishButton(true, R.string.publish);
91 });
92 }
93
94 @Override
95 public void onCreate(final Bundle savedInstanceState) {
96
97 super.onCreate(savedInstanceState);
98
99 this.binding =
100 DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
101
102 setSupportActionBar(binding.toolbar);
103
104 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
105
106 this.binding.publishButton.setOnClickListener(
107 v -> {
108 final boolean open = !this.binding.contactOnly.isChecked();
109 final var uri = this.avatarUri;
110 if (uri == null) {
111 return;
112 }
113 publishing = true;
114 togglePublishButton(false, R.string.publishing);
115 xmppConnectionService.publishAvatar(account, uri, open, this);
116 });
117 this.binding.cancelButton.setOnClickListener(
118 v -> {
119 if (mInitialAccountSetup) {
120 final Intent intent =
121 new Intent(
122 getApplicationContext(), StartConversationActivity.class);
123 if (xmppConnectionService != null
124 && xmppConnectionService.getAccounts().size() == 1) {
125 intent.putExtra("init", true);
126 }
127 StartConversationActivity.addInviteUri(intent, getIntent());
128 if (account != null) {
129 intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
130 }
131 startActivity(intent);
132 }
133 finish();
134 });
135 this.binding.accountImage.setOnClickListener(v -> pickAvatar(null));
136 this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
137 if (savedInstanceState != null) {
138 this.avatarUri = savedInstanceState.getParcelable("uri");
139 }
140 }
141
142 @Override
143 public boolean onCreateOptionsMenu(@NonNull final Menu menu) {
144 getMenuInflater().inflate(R.menu.activity_publish_profile_picture, menu);
145 return true;
146 }
147
148 @Override
149 public boolean onOptionsItemSelected(final MenuItem item) {
150 if (item.getItemId() == R.id.action_delete_avatar) {
151 if (account != null) {
152 deleteAvatar(account);
153 }
154 return true;
155 } else {
156 return super.onOptionsItemSelected(item);
157 }
158 }
159
160 private void deleteAvatar(final Account account) {
161 new MaterialAlertDialogBuilder(this)
162 .setTitle(R.string.delete_avatar)
163 .setMessage(R.string.delete_avatar_message)
164 .setNegativeButton(R.string.cancel, null)
165 .setPositiveButton(
166 R.string.confirm,
167 (d, v) -> {
168 if (xmppConnectionService != null) {
169 xmppConnectionService.deleteAvatar(account);
170 }
171 })
172 .create()
173 .show();
174 }
175
176 @Override
177 public void onSaveInstanceState(@NonNull Bundle outState) {
178 if (this.avatarUri != null) {
179 outState.putParcelable("uri", this.avatarUri);
180 }
181 super.onSaveInstanceState(outState);
182 }
183
184 public void pickAvatar(final Uri image) {
185 this.cropImage.launch(new CropImageContractOptions(image, getCropImageOptions()));
186 }
187
188 public static CropImageOptions getCropImageOptions() {
189 final var cropImageOptions = new CropImageOptions();
190 cropImageOptions.aspectRatioX = 1;
191 cropImageOptions.aspectRatioY = 1;
192 cropImageOptions.fixAspectRatio = true;
193 cropImageOptions.outputCompressFormat = Bitmap.CompressFormat.PNG;
194 cropImageOptions.imageSourceIncludeCamera = false;
195 cropImageOptions.minCropResultHeight = Config.AVATAR_THUMBNAIL_SIZE;
196 cropImageOptions.minCropResultWidth = Config.AVATAR_THUMBNAIL_SIZE;
197 return cropImageOptions;
198 }
199
200 private void onAvatarPicked(final Uri uri) {
201 Log.d(Config.LOGTAG, "onAvatarPicked(" + uri + ")");
202 this.avatarUri = uri;
203 if (xmppConnectionServiceBound) {
204 loadImageIntoPreview(uri);
205 } else {
206 Log.d(Config.LOGTAG, "not ready during avatarPick");
207 }
208 }
209
210 @Override
211 protected void onBackendConnected() {
212 this.account = extractAccount(getIntent());
213 if (this.account != null) {
214 reloadAvatar();
215 }
216 }
217
218 private void reloadAvatar() {
219 this.support =
220 this.account.getXmppConnection() != null
221 && this.account.getXmppConnection().getFeatures().pep();
222 if (this.avatarUri == null) {
223 if (this.account.getAvatar() != null || this.defaultUri == null) {
224 loadImageIntoPreview(null);
225 } else {
226 this.avatarUri = this.defaultUri;
227 loadImageIntoPreview(this.defaultUri);
228 }
229 } else {
230 loadImageIntoPreview(avatarUri);
231 }
232 }
233
234 @Override
235 public void onStart() {
236 super.onStart();
237 final Intent intent = getIntent();
238 if (intent == null) {
239 return;
240 }
241 this.mInitialAccountSetup = intent.getBooleanExtra("setup", false);
242
243 final var data = intent.getData();
244 final var account = intent.getStringExtra(EXTRA_ACCOUNT);
245 if (Intent.ACTION_ATTACH_DATA.equals(intent.getAction())
246 && data != null
247 && account != null) {
248 pickAvatar(data);
249 final var replacement = new Intent(Intent.ACTION_MAIN);
250 replacement.putExtra(EXTRA_ACCOUNT, account);
251 setIntent(replacement);
252 return;
253 }
254
255 if (this.mInitialAccountSetup) {
256 this.binding.cancelButton.setText(R.string.skip);
257 }
258 configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup);
259 }
260
261 protected void loadImageIntoPreview(final Uri uri) {
262 Log.d(Config.LOGTAG, "loadImageIntoPreview(" + uri + ")");
263 final Bitmap bitmap;
264 if (uri == null) {
265 bitmap =
266 avatarService()
267 .get(
268 account,
269 (int) getResources().getDimension(R.dimen.publish_avatar_size));
270 } else {
271 bitmap =
272 xmppConnectionService
273 .getFileBackend()
274 .cropCenterSquare(
275 uri,
276 (int) getResources().getDimension(R.dimen.publish_avatar_size));
277 }
278
279 if (bitmap == null) {
280 togglePublishButton(false, R.string.publish);
281 this.binding.hintOrWarning.setVisibility(View.VISIBLE);
282 this.binding.hintOrWarning.setText(R.string.error_publish_avatar_converting);
283 return;
284 }
285 this.binding.accountImage.setImageBitmap(bitmap);
286 if (support) {
287 togglePublishButton(uri != null, R.string.publish);
288 this.binding.hintOrWarning.setVisibility(View.INVISIBLE);
289 } else {
290 togglePublishButton(false, R.string.publish);
291 this.binding.hintOrWarning.setVisibility(View.VISIBLE);
292 if (account.getStatus() == Account.State.ONLINE) {
293 this.binding.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
294 } else {
295 this.binding.hintOrWarning.setText(R.string.error_publish_avatar_offline);
296 }
297 }
298 if (this.defaultUri == null || this.defaultUri.equals(uri)) {
299 this.binding.secondaryHint.setVisibility(View.INVISIBLE);
300 this.binding.accountImage.setOnLongClickListener(null);
301 } else if (this.defaultUri != null) {
302 this.binding.secondaryHint.setVisibility(View.VISIBLE);
303 this.binding.accountImage.setOnLongClickListener(this.backToDefaultListener);
304 }
305 }
306
307 protected void togglePublishButton(boolean enabled, @StringRes int res) {
308 final boolean status = enabled && !publishing;
309 this.binding.publishButton.setText(publishing ? R.string.publishing : res);
310 this.binding.publishButton.setEnabled(status);
311 }
312
313 public void refreshUiReal() {
314 if (this.account != null) {
315 reloadAvatar();
316 }
317 }
318
319 @Override
320 public void onAccountUpdate() {
321 refreshUi();
322 }
323}