PublishProfilePictureActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.content.Intent;
  4import android.graphics.Bitmap;
  5import android.net.Uri;
  6import android.os.Bundle;
  7import android.util.Log;
  8import android.view.Menu;
  9import android.view.MenuItem;
 10import android.view.View;
 11import android.view.View.OnLongClickListener;
 12import android.widget.Toast;
 13import androidx.activity.result.ActivityResultLauncher;
 14import androidx.annotation.NonNull;
 15import androidx.annotation.StringRes;
 16import androidx.core.content.ContextCompat;
 17import androidx.databinding.DataBindingUtil;
 18import com.canhub.cropper.CropImageContract;
 19import com.canhub.cropper.CropImageContractOptions;
 20import com.canhub.cropper.CropImageOptions;
 21import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 22import com.google.common.util.concurrent.FutureCallback;
 23import com.google.common.util.concurrent.Futures;
 24import com.google.common.util.concurrent.ListenableFuture;
 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.services.XmppConnectionService;
 30import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 31import eu.siacs.conversations.utils.PhoneHelper;
 32import eu.siacs.conversations.xmpp.manager.AvatarManager;
 33import im.conversations.android.xmpp.NodeConfiguration;
 34
 35public class PublishProfilePictureActivity extends XmppActivity
 36        implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
 37
 38    private ActivityPublishProfilePictureBinding binding;
 39    private Uri avatarUri;
 40    private NodeConfiguration.AccessModel accessModel;
 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.publishAvatar(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                        if (xmppConnectionService != null
129                                && xmppConnectionService.getAccounts().size() == 1) {
130                            intent.putExtra("init", true);
131                        }
132                        StartConversationActivity.addInviteUri(intent, getIntent());
133                        if (account != null) {
134                            intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
135                        }
136                        startActivity(intent);
137                    }
138                    finish();
139                });
140        this.binding.accountImage.setOnClickListener(v -> pickAvatar(null));
141        this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
142        if (savedInstanceState != null) {
143            this.avatarUri = savedInstanceState.getParcelable("uri");
144            final var accessModel = savedInstanceState.getString("access-model");
145            if (accessModel != null) {
146                this.accessModel = NodeConfiguration.AccessModel.valueOf(accessModel);
147            }
148        }
149    }
150
151    @Override
152    public boolean onCreateOptionsMenu(@NonNull final Menu menu) {
153        getMenuInflater().inflate(R.menu.activity_publish_profile_picture, menu);
154        return true;
155    }
156
157    @Override
158    public boolean onOptionsItemSelected(final MenuItem item) {
159        if (item.getItemId() == R.id.action_delete_avatar) {
160            if (account != null) {
161                deleteAvatar(account);
162            }
163            return true;
164        } else {
165            return super.onOptionsItemSelected(item);
166        }
167    }
168
169    private void deleteAvatar(final Account account) {
170        new MaterialAlertDialogBuilder(this)
171                .setTitle(R.string.delete_avatar)
172                .setMessage(R.string.delete_avatar_message)
173                .setNegativeButton(R.string.cancel, null)
174                .setPositiveButton(
175                        R.string.confirm,
176                        (d, v) -> {
177                            if (xmppConnectionService != null) {
178                                xmppConnectionService.deleteAvatar(account);
179                            }
180                        })
181                .create()
182                .show();
183    }
184
185    @Override
186    public void onSaveInstanceState(@NonNull Bundle outState) {
187        if (this.avatarUri != null) {
188            outState.putParcelable("uri", this.avatarUri);
189        }
190        if (this.accessModel != null) {
191            outState.putString("access-model", this.accessModel.toString());
192        }
193        super.onSaveInstanceState(outState);
194    }
195
196    public void pickAvatar(final Uri image) {
197        this.cropImage.launch(new CropImageContractOptions(image, getCropImageOptions()));
198    }
199
200    public static CropImageOptions getCropImageOptions() {
201        final var cropImageOptions = new CropImageOptions();
202        cropImageOptions.aspectRatioX = 1;
203        cropImageOptions.aspectRatioY = 1;
204        cropImageOptions.fixAspectRatio = true;
205        cropImageOptions.outputCompressFormat = Bitmap.CompressFormat.PNG;
206        cropImageOptions.imageSourceIncludeCamera = false;
207        cropImageOptions.minCropResultHeight = Config.AVATAR_THUMBNAIL_SIZE;
208        cropImageOptions.minCropResultWidth = Config.AVATAR_THUMBNAIL_SIZE;
209        return cropImageOptions;
210    }
211
212    private void onAvatarPicked(final Uri uri) {
213        Log.d(Config.LOGTAG, "onAvatarPicked(" + uri + ")");
214        this.avatarUri = uri;
215        if (xmppConnectionServiceBound) {
216            loadImageIntoPreview(uri);
217        } else {
218            Log.d(Config.LOGTAG, "not ready during avatarPick");
219        }
220    }
221
222    @Override
223    protected void onBackendConnected() {
224        final var account = extractAccount(getIntent());
225        this.account = account;
226        if (account != null) {
227            loadCurrentAccessModel(account);
228            reloadAvatar(account);
229        }
230    }
231
232    private void loadCurrentAccessModel(final Account account) {
233        binding.contactOnly.setVisibility(View.INVISIBLE);
234        final var currentPepAccessModel = getPepAccessModelOrCached(account);
235        Futures.addCallback(
236                currentPepAccessModel,
237                new FutureCallback<>() {
238                    @Override
239                    public void onSuccess(final NodeConfiguration.AccessModel result) {
240                        accessModel = result; // cache for after rotation
241                        Log.d(Config.LOGTAG, "current access model: " + result);
242                        binding.contactOnly.setChecked(
243                                result == NodeConfiguration.AccessModel.PRESENCE);
244                        binding.contactOnly.jumpDrawablesToCurrentState();
245                        binding.contactOnly.setVisibility(View.VISIBLE);
246                    }
247
248                    @Override
249                    public void onFailure(@NonNull Throwable t) {
250                        Log.d(Config.LOGTAG, "could not fetch access model", t);
251                        binding.contactOnly.setChecked(false);
252                        binding.contactOnly.setVisibility(View.VISIBLE);
253                    }
254                },
255                ContextCompat.getMainExecutor(getApplication()));
256    }
257
258    private ListenableFuture<NodeConfiguration.AccessModel> getPepAccessModelOrCached(
259            final Account account) {
260        final var cached = this.accessModel;
261        if (cached != null) {
262            return Futures.immediateFuture(cached);
263        }
264        return account.getXmppConnection().getManager(AvatarManager.class).getPepAccessModel();
265    }
266
267    private void reloadAvatar() {
268        reloadAvatar(this.account);
269    }
270
271    private void reloadAvatar(final Account account) {
272        this.support = account.getXmppConnection().getFeatures().pep();
273        if (this.avatarUri == null) {
274            if (account.getAvatar() != null || this.defaultUri == null) {
275                loadImageIntoPreview(null);
276            } else {
277                this.avatarUri = this.defaultUri;
278                loadImageIntoPreview(this.defaultUri);
279            }
280        } else {
281            loadImageIntoPreview(avatarUri);
282        }
283    }
284
285    @Override
286    public void onStart() {
287        super.onStart();
288        final Intent intent = getIntent();
289        if (intent == null) {
290            return;
291        }
292        this.mInitialAccountSetup = intent.getBooleanExtra("setup", false);
293
294        final var data = intent.getData();
295        final var account = intent.getStringExtra(EXTRA_ACCOUNT);
296        if (Intent.ACTION_ATTACH_DATA.equals(intent.getAction())
297                && data != null
298                && account != null) {
299            pickAvatar(data);
300            final var replacement = new Intent(Intent.ACTION_MAIN);
301            replacement.putExtra(EXTRA_ACCOUNT, account);
302            setIntent(replacement);
303            return;
304        }
305
306        if (this.mInitialAccountSetup) {
307            this.binding.cancelButton.setText(R.string.skip);
308        }
309        configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup);
310    }
311
312    protected void loadImageIntoPreview(final Uri uri) {
313        Log.d(Config.LOGTAG, "loadImageIntoPreview(" + uri + ")");
314        final Bitmap bitmap;
315        if (uri == null) {
316            bitmap =
317                    avatarService()
318                            .get(
319                                    account,
320                                    (int) getResources().getDimension(R.dimen.publish_avatar_size));
321        } else {
322            bitmap =
323                    xmppConnectionService
324                            .getFileBackend()
325                            .cropCenterSquare(
326                                    uri,
327                                    (int) getResources().getDimension(R.dimen.publish_avatar_size));
328        }
329
330        if (bitmap == null) {
331            togglePublishButton(false, R.string.publish);
332            this.binding.hintOrWarning.setVisibility(View.VISIBLE);
333            this.binding.hintOrWarning.setText(R.string.error_publish_avatar_converting);
334            return;
335        }
336        this.binding.accountImage.setImageBitmap(bitmap);
337        if (support) {
338            togglePublishButton(uri != null, R.string.publish);
339            this.binding.hintOrWarning.setVisibility(View.INVISIBLE);
340        } else {
341            togglePublishButton(false, R.string.publish);
342            this.binding.hintOrWarning.setVisibility(View.VISIBLE);
343            if (account.getStatus() == Account.State.ONLINE) {
344                this.binding.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
345            } else {
346                this.binding.hintOrWarning.setText(R.string.error_publish_avatar_offline);
347            }
348        }
349        if (this.defaultUri == null || this.defaultUri.equals(uri)) {
350            this.binding.secondaryHint.setVisibility(View.INVISIBLE);
351            this.binding.accountImage.setOnLongClickListener(null);
352        } else if (this.defaultUri != null) {
353            this.binding.secondaryHint.setVisibility(View.VISIBLE);
354            this.binding.accountImage.setOnLongClickListener(this.backToDefaultListener);
355        }
356    }
357
358    protected void togglePublishButton(boolean enabled, @StringRes int res) {
359        final boolean status = enabled && !publishing;
360        this.binding.publishButton.setText(publishing ? R.string.publishing : res);
361        this.binding.publishButton.setEnabled(status);
362        this.binding.contactOnly.setEnabled(status);
363    }
364
365    public void refreshUiReal() {
366        if (this.account != null) {
367            reloadAvatar();
368        }
369    }
370
371    @Override
372    public void onAccountUpdate() {
373        refreshUi();
374    }
375}