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