PublishProfilePictureActivity.java

  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}