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