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.core.content.ContextCompat;
 21import androidx.databinding.DataBindingUtil;
 22import com.canhub.cropper.CropImageContract;
 23import com.canhub.cropper.CropImageContractOptions;
 24import com.canhub.cropper.CropImageOptions;
 25import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 26import com.google.common.util.concurrent.FutureCallback;
 27import com.google.common.util.concurrent.Futures;
 28import com.google.common.util.concurrent.ListenableFuture;
 29import eu.siacs.conversations.Config;
 30import eu.siacs.conversations.R;
 31import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
 32import eu.siacs.conversations.entities.Account;
 33import eu.siacs.conversations.persistance.FileBackend;
 34import eu.siacs.conversations.services.XmppConnectionService;
 35import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 36import eu.siacs.conversations.utils.PhoneHelper;
 37import eu.siacs.conversations.xmpp.manager.AvatarManager;
 38import im.conversations.android.xmpp.NodeConfiguration;
 39
 40public class PublishProfilePictureActivity extends XmppActivity
 41        implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
 42
 43    private ActivityPublishProfilePictureBinding binding;
 44    private Uri avatarUri;
 45    private NodeConfiguration.AccessModel accessModel;
 46    private Uri defaultUri;
 47    private Account account;
 48    private boolean support = false;
 49    private boolean publishing = false;
 50    private final OnLongClickListener backToDefaultListener =
 51            new OnLongClickListener() {
 52
 53                @Override
 54                public boolean onLongClick(View v) {
 55                    avatarUri = defaultUri;
 56                    loadImageIntoPreview(defaultUri);
 57                    return true;
 58                }
 59            };
 60    private boolean mInitialAccountSetup;
 61
 62    final ActivityResultLauncher<CropImageContractOptions> cropImage =
 63            registerForActivityResult(
 64                    new CropImageContract(),
 65                    cropResult -> {
 66                        if (cropResult.isSuccessful()) {
 67                            onAvatarPicked(cropResult.getUriContent());
 68                        }
 69                    });
 70
 71    @Override
 72    public void onAvatarPublicationSucceeded() {
 73        runOnUiThread(
 74                () -> {
 75                    if (mInitialAccountSetup) {
 76                        Intent intent =
 77                                new Intent(
 78                                        getApplicationContext(), StartConversationActivity.class);
 79                        StartConversationActivity.addInviteUri(intent, getIntent());
 80                        intent.putExtra("init", true);
 81                        intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
 82                        startActivity(intent);
 83                    }
 84                    Toast.makeText(
 85                                    PublishProfilePictureActivity.this,
 86                                    R.string.avatar_has_been_published,
 87                                    Toast.LENGTH_SHORT)
 88                            .show();
 89                    finish();
 90                });
 91    }
 92
 93    @Override
 94    public void onAvatarPublicationFailed(final int res) {
 95        runOnUiThread(
 96                () -> {
 97                    this.binding.hintOrWarning.setText(res);
 98                    this.binding.hintOrWarning.setVisibility(View.VISIBLE);
 99                    this.publishing = false;
100                    togglePublishButton(true, R.string.publish);
101                });
102    }
103
104    @Override
105    public void onCreate(final Bundle savedInstanceState) {
106
107        super.onCreate(savedInstanceState);
108
109        this.binding =
110                DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
111
112        setSupportActionBar(binding.toolbar);
113
114        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
115
116        this.binding.publishButton.setOnClickListener(
117                v -> {
118                    final boolean open = !this.binding.contactOnly.isChecked();
119                    final var uri = this.avatarUri;
120                    if (uri == null) {
121                        return;
122                    }
123                    publishing = true;
124                    togglePublishButton(false, R.string.publishing);
125                    xmppConnectionService.publishAvatar(account, uri, open, this);
126                });
127        this.binding.cancelButton.setOnClickListener(
128                v -> {
129                    if (mInitialAccountSetup) {
130                        final Intent intent =
131                                new Intent(
132                                        getApplicationContext(), StartConversationActivity.class);
133                        intent.putExtra("init", true);
134                        StartConversationActivity.addInviteUri(intent, getIntent());
135                        if (account != null) {
136                            intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
137                        }
138                        startActivity(intent);
139                    }
140                    finish();
141                });
142        this.binding.accountImage.setOnClickListener(v -> pickAvatar(null));
143        this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
144        if (savedInstanceState != null) {
145            this.avatarUri = savedInstanceState.getParcelable("uri");
146            final var accessModel = savedInstanceState.getString("access-model");
147            if (accessModel != null) {
148                this.accessModel = NodeConfiguration.AccessModel.valueOf(accessModel);
149            }
150        }
151    }
152
153    @Override
154    public boolean onCreateOptionsMenu(@NonNull final Menu menu) {
155        getMenuInflater().inflate(R.menu.activity_publish_profile_picture, menu);
156        return true;
157    }
158
159    @Override
160    public boolean onOptionsItemSelected(final MenuItem item) {
161        if (item.getItemId() == R.id.action_delete_avatar) {
162            if (account != null) {
163                deleteAvatar(account);
164            }
165            return true;
166        } else {
167            return super.onOptionsItemSelected(item);
168        }
169    }
170
171    private void deleteAvatar(final Account account) {
172        new MaterialAlertDialogBuilder(this)
173                .setTitle(R.string.delete_avatar)
174                .setMessage(R.string.delete_avatar_message)
175                .setNegativeButton(R.string.cancel, null)
176                .setPositiveButton(
177                        R.string.confirm,
178                        (d, v) -> {
179                            if (xmppConnectionService != null) {
180                                xmppConnectionService.deleteAvatar(account);
181                            }
182                        })
183                .create()
184                .show();
185    }
186
187    @Override
188    public void onSaveInstanceState(@NonNull Bundle outState) {
189        if (this.avatarUri != null) {
190            outState.putParcelable("uri", this.avatarUri);
191        }
192        if (this.accessModel != null) {
193            outState.putString("access-model", this.accessModel.toString());
194        }
195        super.onSaveInstanceState(outState);
196    }
197
198    public void pickAvatar(final Uri image) {
199        this.cropImage.launch(new CropImageContractOptions(image, getCropImageOptions()));
200    }
201
202    public static CropImageOptions getCropImageOptions() {
203        final var cropImageOptions = new CropImageOptions();
204        cropImageOptions.aspectRatioX = 1;
205        cropImageOptions.aspectRatioY = 1;
206        cropImageOptions.fixAspectRatio = true;
207        cropImageOptions.outputCompressFormat = Bitmap.CompressFormat.PNG;
208        cropImageOptions.imageSourceIncludeCamera = false;
209        cropImageOptions.minCropResultHeight = Config.AVATAR_THUMBNAIL_SIZE;
210        cropImageOptions.minCropResultWidth = Config.AVATAR_THUMBNAIL_SIZE;
211        return cropImageOptions;
212    }
213
214    private void onAvatarPicked(final Uri uri) {
215        Log.d(Config.LOGTAG, "onAvatarPicked(" + uri + ")");
216        this.avatarUri = uri;
217        if (xmppConnectionServiceBound) {
218            loadImageIntoPreview(uri);
219        } else {
220            Log.d(Config.LOGTAG, "not ready during avatarPick");
221        }
222    }
223
224    @Override
225    protected void onBackendConnected() {
226        final var account = extractAccount(getIntent());
227        this.account = account;
228        if (account != null) {
229            loadCurrentAccessModel(account);
230            reloadAvatar(account);
231        }
232    }
233
234    private void loadCurrentAccessModel(final Account account) {
235        binding.contactOnly.setVisibility(View.INVISIBLE);
236        final var currentPepAccessModel = getPepAccessModelOrCached(account);
237        Futures.addCallback(
238                currentPepAccessModel,
239                new FutureCallback<>() {
240                    @Override
241                    public void onSuccess(final NodeConfiguration.AccessModel result) {
242                        accessModel = result; // cache for after rotation
243                        Log.d(Config.LOGTAG, "current access model: " + result);
244                        binding.contactOnly.setChecked(
245                                result == NodeConfiguration.AccessModel.PRESENCE);
246                        binding.contactOnly.jumpDrawablesToCurrentState();
247                        binding.contactOnly.setVisibility(View.VISIBLE);
248                    }
249
250                    @Override
251                    public void onFailure(@NonNull Throwable t) {
252                        Log.d(Config.LOGTAG, "could not fetch access model", t);
253                        binding.contactOnly.setChecked(false);
254                        binding.contactOnly.setVisibility(View.VISIBLE);
255                    }
256                },
257                ContextCompat.getMainExecutor(getApplication()));
258    }
259
260    private ListenableFuture<NodeConfiguration.AccessModel> getPepAccessModelOrCached(
261            final Account account) {
262        final var cached = this.accessModel;
263        if (cached != null) {
264            return Futures.immediateFuture(cached);
265        }
266        return account.getXmppConnection().getManager(AvatarManager.class).getPepAccessModel();
267    }
268
269    private void reloadAvatar() {
270        reloadAvatar(this.account);
271    }
272
273    private void reloadAvatar(final Account account) {
274        this.support = account.getXmppConnection().getFeatures().pep();
275        if (this.avatarUri == null) {
276            if (account.getAvatar() != null || this.defaultUri == null) {
277                loadImageIntoPreview(null);
278            } else {
279                this.avatarUri = this.defaultUri;
280                loadImageIntoPreview(this.defaultUri);
281            }
282        } else {
283            loadImageIntoPreview(avatarUri);
284        }
285    }
286
287    @Override
288    public void onStart() {
289        super.onStart();
290        final Intent intent = getIntent();
291        if (intent == null) {
292            return;
293        }
294        this.mInitialAccountSetup = intent.getBooleanExtra("setup", false);
295
296        final var data = intent.getData();
297        final var account = intent.getStringExtra(EXTRA_ACCOUNT);
298        if (Intent.ACTION_ATTACH_DATA.equals(intent.getAction())
299                && data != null
300                && account != null) {
301            pickAvatar(data);
302            final var replacement = new Intent(Intent.ACTION_MAIN);
303            replacement.putExtra(EXTRA_ACCOUNT, account);
304            setIntent(replacement);
305            return;
306        }
307
308        if (this.mInitialAccountSetup) {
309            this.binding.cancelButton.setText(R.string.skip);
310        }
311        configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup);
312    }
313
314    protected void loadImageIntoPreview(final Uri uri) {
315        Drawable bm = null;
316        if (uri == null) {
317            bm =
318                    avatarService()
319                            .get(
320                                    account,
321                                    (int) getResources().getDimension(R.dimen.publish_avatar_size));
322        } else {
323            try {
324                bm =
325                        xmppConnectionService
326                                .getFileBackend()
327                                .cropCenterSquareDrawable(
328                                        uri,
329                                        (int)
330                                                getResources()
331                                                        .getDimension(R.dimen.publish_avatar_size));
332            } catch (final Exception e) {
333                Log.d(Config.LOGTAG, "unable to load bitmap into image view", e);
334            }
335        }
336
337        if (bm == null) {
338            togglePublishButton(false, R.string.publish);
339            this.binding.hintOrWarning.setVisibility(View.VISIBLE);
340            this.binding.hintOrWarning.setText(R.string.error_publish_avatar_converting);
341            return;
342        }
343        this.binding.accountImage.setImageDrawable(bm);
344        if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
345            ((AnimatedImageDrawable) bm).start();
346        }
347        if (support) {
348            togglePublishButton(uri != null, R.string.publish);
349            this.binding.hintOrWarning.setVisibility(View.INVISIBLE);
350        } else {
351            togglePublishButton(false, R.string.publish);
352            this.binding.hintOrWarning.setVisibility(View.VISIBLE);
353            if (account.getStatus() == Account.State.ONLINE) {
354                this.binding.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
355            } else {
356                this.binding.hintOrWarning.setText(R.string.error_publish_avatar_offline);
357            }
358        }
359        if (this.defaultUri == null || this.defaultUri.equals(uri)) {
360            this.binding.secondaryHint.setVisibility(View.INVISIBLE);
361            this.binding.accountImage.setOnLongClickListener(null);
362        } else if (this.defaultUri != null) {
363            this.binding.secondaryHint.setVisibility(View.VISIBLE);
364            this.binding.accountImage.setOnLongClickListener(this.backToDefaultListener);
365        }
366    }
367
368    protected void togglePublishButton(boolean enabled, @StringRes int res) {
369        final boolean status = enabled && !publishing;
370        this.binding.publishButton.setText(publishing ? R.string.publishing : res);
371        this.binding.publishButton.setEnabled(status);
372        this.binding.contactOnly.setEnabled(status);
373    }
374
375    public void refreshUiReal() {
376        if (this.account != null) {
377            reloadAvatar();
378        }
379    }
380
381    @Override
382    public void onAccountUpdate() {
383        refreshUi();
384    }
385}