Fix null pointer exception when presenting latest media thumbnail.

This commit is contained in:
Alex Hart 2022-07-27 12:49:16 -03:00 committed by Cody Henthorne
parent 053b0eabde
commit c907a01077
6 changed files with 38 additions and 45 deletions

View file

@ -13,7 +13,6 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.LiveData;
import org.signal.imageeditor.core.model.EditorModel; import org.signal.imageeditor.core.model.EditorModel;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@ -22,13 +21,14 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment; import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.util.Collections; import java.util.Collections;
import java.util.Optional; import java.util.Optional;
import io.reactivex.rxjava3.core.Flowable;
public class AvatarSelectionActivity extends AppCompatActivity implements CameraFragment.Controller, ImageEditorFragment.Controller, MediaGalleryFragment.Callbacks { public class AvatarSelectionActivity extends AppCompatActivity implements CameraFragment.Controller, ImageEditorFragment.Controller, MediaGalleryFragment.Callbacks {
private static final Point AVATAR_DIMENSIONS = new Point(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS); private static final Point AVATAR_DIMENSIONS = new Point(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS);
@ -131,8 +131,8 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
} }
@Override @Override
public @NonNull LiveData<Optional<Media>> getMostRecentMediaItem() { public @NonNull Flowable<Optional<Media>> getMostRecentMediaItem() {
return new DefaultValueLiveData<>(Optional.empty()); return Flowable.just(Optional.empty());
} }
@Override @Override

View file

@ -27,7 +27,6 @@ import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.Observer;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.MultiTransformation; import com.bumptech.glide.load.MultiTransformation;
@ -51,8 +50,8 @@ import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.Optional;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
/** /**
@ -75,8 +74,8 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment,
private Camera1Controller.Properties properties; private Camera1Controller.Properties properties;
private RotationListener rotationListener; private RotationListener rotationListener;
private Disposable rotationListenerDisposable; private Disposable rotationListenerDisposable;
private Disposable mostRecentItemDisposable = Disposable.disposed();
private final Observer<Optional<Media>> thumbObserver = this::presentRecentItemThumbnail;
private boolean isThumbAvailable; private boolean isThumbAvailable;
private boolean isMediaSelected; private boolean isMediaSelected;
@ -192,7 +191,7 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment,
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
controller.getMostRecentMediaItem().removeObserver(thumbObserver); mostRecentItemDisposable.dispose();
} }
@Override @Override
@ -271,19 +270,13 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment,
controller.onCameraError(); controller.onCameraError();
} }
private void presentRecentItemThumbnail(Optional<Media> media) { private void presentRecentItemThumbnail(@Nullable Media media) {
if (media == null) {
isThumbAvailable = false;
updateGalleryVisibility();
return;
}
ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button); ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
if (media.isPresent()) { if (media != null) {
thumbnail.setVisibility(View.VISIBLE); thumbnail.setVisibility(View.VISIBLE);
Glide.with(this) Glide.with(this)
.load(new DecryptableUri(media.get().getUri())) .load(new DecryptableUri(media.getUri()))
.centerCrop() .centerCrop()
.into(thumbnail); .into(thumbnail);
} else { } else {
@ -291,7 +284,7 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment,
thumbnail.setImageResource(0); thumbnail.setImageResource(0);
} }
isThumbAvailable = media.isPresent(); isThumbAvailable = media != null;
updateGalleryVisibility(); updateGalleryVisibility();
} }
@ -331,8 +324,10 @@ public class Camera1Fragment extends LoggingFragment implements CameraFragment,
View countButton = requireView().findViewById(R.id.camera_review_button); View countButton = requireView().findViewById(R.id.camera_review_button);
View toggleSpacer = requireView().findViewById(R.id.toggle_spacer); View toggleSpacer = requireView().findViewById(R.id.toggle_spacer);
controller.getMostRecentMediaItem().removeObserver(thumbObserver); mostRecentItemDisposable.dispose();
controller.getMostRecentMediaItem().observeForever(thumbObserver); mostRecentItemDisposable = controller.getMostRecentMediaItem()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(item -> presentRecentItemThumbnail(item.orElse(null)));
if (toggleSpacer != null) { if (toggleSpacer != null) {
if (Stories.isFeatureEnabled()) { if (Stories.isFeatureEnabled()) {

View file

@ -5,7 +5,6 @@ import android.content.res.Configuration;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil; import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaConstraints;
@ -13,6 +12,8 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.util.Optional; import java.util.Optional;
import io.reactivex.rxjava3.core.Flowable;
public interface CameraFragment { public interface CameraFragment {
float PORTRAIT_ASPECT_RATIO = 9 / 16f; float PORTRAIT_ASPECT_RATIO = 9 / 16f;
@ -54,7 +55,7 @@ public interface CameraFragment {
void onVideoCaptureError(); void onVideoCaptureError();
void onGalleryClicked(); void onGalleryClicked();
void onCameraCountButtonClicked(); void onCameraCountButtonClicked();
@NonNull LiveData<Optional<Media>> getMostRecentMediaItem(); @NonNull Flowable<Optional<Media>> getMostRecentMediaItem();
@NonNull MediaConstraints getMediaConstraints(); @NonNull MediaConstraints getMediaConstraints();
int getMaxVideoDuration(); int getMaxVideoDuration();
} }

View file

@ -31,7 +31,6 @@ import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView; import androidx.camera.view.PreviewView;
import androidx.camera.view.SignalCameraView; import androidx.camera.view.SignalCameraView;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.Observer;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.util.Executors; import com.bumptech.glide.util.Executors;
@ -57,7 +56,9 @@ import org.thoughtcrime.securesms.video.VideoUtil;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
/** /**
* Camera captured implemented using the CameraX SDK, which uses Camera2 under the hood. Should be * Camera captured implemented using the CameraX SDK, which uses Camera2 under the hood. Should be
@ -74,8 +75,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
private Controller controller; private Controller controller;
private View selfieFlash; private View selfieFlash;
private MemoryFileDescriptor videoFileDescriptor; private MemoryFileDescriptor videoFileDescriptor;
private Disposable mostRecentItemDisposable = Disposable.disposed();
private final Observer<Optional<Media>> thumbObserver = this::presentRecentItemThumbnail;
private boolean isThumbAvailable; private boolean isThumbAvailable;
private boolean isMediaSelected; private boolean isMediaSelected;
@ -161,7 +162,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
controller.getMostRecentMediaItem().removeObserver(thumbObserver); mostRecentItemDisposable.dispose();
closeVideoFileDescriptor(); closeVideoFileDescriptor();
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
} }
@ -221,19 +222,13 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
initControls(); initControls();
} }
private void presentRecentItemThumbnail(Optional<Media> media) { private void presentRecentItemThumbnail(@Nullable Media media) {
if (media == null) {
isThumbAvailable = false;
updateGalleryVisibility();
return;
}
ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button); ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
if (media.isPresent()) { if (media != null) {
thumbnail.setVisibility(View.VISIBLE); thumbnail.setVisibility(View.VISIBLE);
Glide.with(this) Glide.with(this)
.load(new DecryptableUri(media.get().getUri())) .load(new DecryptableUri(media.getUri()))
.centerCrop() .centerCrop()
.into(thumbnail); .into(thumbnail);
} else { } else {
@ -241,7 +236,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
thumbnail.setImageResource(0); thumbnail.setImageResource(0);
} }
isThumbAvailable = media.isPresent(); isThumbAvailable = media != null;
updateGalleryVisibility(); updateGalleryVisibility();
} }
@ -292,8 +287,10 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
} }
} }
controller.getMostRecentMediaItem().removeObserver(thumbObserver); mostRecentItemDisposable.dispose();
controller.getMostRecentMediaItem().observeForever(thumbObserver); mostRecentItemDisposable = controller.getMostRecentMediaItem()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(item -> presentRecentItemThumbnail(item.orElse(null)));
selfieFlash = requireView().findViewById(R.id.camera_selfie_flash); selfieFlash = requireView().findViewById(R.id.camera_selfie_flash);

View file

@ -6,9 +6,9 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import app.cash.exhaustive.Exhaustive import app.cash.exhaustive.Exhaustive
import io.reactivex.rxjava3.core.Flowable
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.CameraFragment import org.thoughtcrime.securesms.mediasend.CameraFragment
@ -149,7 +149,7 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme
} }
} }
override fun getMostRecentMediaItem(): LiveData<Optional<Media>> { override fun getMostRecentMediaItem(): Flowable<Optional<Media>> {
return viewModel.getMostRecentMedia() return viewModel.getMostRecentMedia()
} }

View file

@ -1,18 +1,18 @@
package org.thoughtcrime.securesms.mediasend.v2.capture package org.thoughtcrime.securesms.mediasend.v2.capture
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Flowable
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.util.SingleLiveEvent import org.thoughtcrime.securesms.util.SingleLiveEvent
import org.thoughtcrime.securesms.util.livedata.Store import org.thoughtcrime.securesms.util.rx.RxStore
import java.io.FileDescriptor import java.io.FileDescriptor
import java.util.Optional import java.util.Optional
class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : ViewModel() { class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : ViewModel() {
private val store: Store<MediaCaptureState> = Store(MediaCaptureState()) private val store: RxStore<MediaCaptureState> = RxStore(MediaCaptureState())
private val internalEvents: SingleLiveEvent<MediaCaptureEvent> = SingleLiveEvent() private val internalEvents: SingleLiveEvent<MediaCaptureEvent> = SingleLiveEvent()
@ -34,8 +34,8 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
repository.renderVideoToMedia(fd, this::onMediaRendered, this::onMediaRenderFailed) repository.renderVideoToMedia(fd, this::onMediaRendered, this::onMediaRenderFailed)
} }
fun getMostRecentMedia(): LiveData<Optional<Media>> { fun getMostRecentMedia(): Flowable<Optional<Media>> {
return Transformations.map(store.stateLiveData) { Optional.ofNullable(it.mostRecentMedia) } return store.stateFlowable.map { Optional.ofNullable(it.mostRecentMedia) }
} }
private fun onMediaRendered(media: Media) { private fun onMediaRendered(media: Media) {