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