update fork #128

Manually merged
tristan merged 181 commits from mirror/monocles_chat_clean:master into master 2026-01-23 14:02:38 +01:00
4 changed files with 215 additions and 217 deletions
Showing only changes of commit 88b3c99107 - Show all commits

Migrate StoryViewActivity to ViewPager2

Arne 2026-01-02 16:22:28 +01:00

View file

@ -0,0 +1,65 @@
package eu.siacs.conversations.ui;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.VideoView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.bumptech.glide.Glide;
import java.io.File;
import eu.siacs.conversations.R;
public class StoryFragment extends Fragment {
private static final String ARG_URL = "url";
private static final String ARG_MIME_TYPE = "mime_type";
public static StoryFragment newInstance(String url, String mimeType) {
StoryFragment fragment = new StoryFragment();
Bundle args = new Bundle();
args.putString(ARG_URL, url);
args.putString(ARG_MIME_TYPE, mimeType);
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_story, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ImageView imageView = view.findViewById(R.id.story_image_view);
VideoView videoView = view.findViewById(R.id.story_video_view);
String url = getArguments().getString(ARG_URL);
String mimeType = getArguments().getString(ARG_MIME_TYPE);
if (mimeType != null && mimeType.startsWith("video/")) {
imageView.setVisibility(View.GONE);
videoView.setVisibility(View.VISIBLE);
videoView.setVideoURI(Uri.parse(url));
videoView.setOnPreparedListener(mp -> {
mp.setLooping(true);
videoView.start();
});
} else {
videoView.setVisibility(View.GONE);
imageView.setVisibility(View.VISIBLE);
Glide.with(this).load(url).into(imageView);
}
}
}

View file

@ -1,44 +1,37 @@
package eu.siacs.conversations.ui;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.VideoView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.Glide;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.widget.AvatarView;
import eu.siacs.conversations.xmpp.Jid;
import okhttp3.HttpUrl;
public class StoryViewActivity extends XmppActivity {
@ -49,8 +42,7 @@ public class StoryViewActivity extends XmppActivity {
public static final String EXTRA_CONTACT = "contact";
public static final String EXTRA_MIME_TYPES = "story_mime_types";
private ImageView imageView;
private VideoView videoView;
private ViewPager2 viewPager;
private TextView titleView;
private TextView progressView;
private View bottomPanel;
@ -59,10 +51,10 @@ public class StoryViewActivity extends XmppActivity {
private ArrayList<String> titles;
private ArrayList<String> storyIds;
private ArrayList<String> mimeTypes;
private int currentIndex = 0;
private Jid contact;
private Account mAccount;
private Message storyMessage;
private GestureDetector gestureDetector;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -76,8 +68,7 @@ public class StoryViewActivity extends XmppActivity {
getSupportActionBar().setDisplayShowTitleEnabled(false);
}
imageView = findViewById(R.id.story_image_view);
videoView = findViewById(R.id.story_video_view);
viewPager = findViewById(R.id.view_pager);
titleView = findViewById(R.id.story_title_view);
progressView = findViewById(R.id.story_progress_view);
bottomPanel = findViewById(R.id.bottom_panel);
@ -87,125 +78,43 @@ public class StoryViewActivity extends XmppActivity {
storyIds = getIntent().getStringArrayListExtra(EXTRA_STORY_IDS);
mimeTypes = getIntent().getStringArrayListExtra(EXTRA_MIME_TYPES);
View.OnTouchListener touchListener = (v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (event.getX() < v.getWidth() / 3) {
currentIndex--;
if (currentIndex >= 0) {
loadStory();
} else {
finish();
}
} else if (event.getX() > v.getWidth() * 2 / 3) {
currentIndex++;
if (currentIndex < urls.size()) {
loadStory();
} else {
finish();
}
} else {
if (isSystemUiVisible()) {
hideSystemUi();
} else {
showSystemUi();
}
}
}
return true;
};
imageView.setOnTouchListener(touchListener);
videoView.setOnTouchListener(touchListener);
try {
contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT));
} catch (final Exception e) {
//ignore
}
}
class GestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
protected void refreshUiReal() {
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_story_view, menu);
MenuItem deleteButton = menu.findItem(R.id.action_delete_story);
if (contact != null && xmppConnectionService != null) {
final Account storyOwner = xmppConnectionService.findAccountByJid(contact);
if (storyOwner != null && storyOwner.isOnlineAndConnected()) {
deleteButton.setVisible(true);
this.mAccount = storyOwner;
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (isSystemUiVisible()) {
hideSystemUi();
} else {
showSystemUi();
}
return true;
}
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.action_delete_story) {
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.delete_story_dialog_title)
.setMessage(R.string.delete_story_dialog_message)
.setPositiveButton(R.string.delete, (dialog, which) -> {
xmppConnectionService.retractStory(mAccount, storyIds.get(currentIndex), new UiCallback<Void>() {
@Override
public void success(Void aVoid) {
runOnUiThread(() -> {
Toast.makeText(StoryViewActivity.this, R.string.story_deleted, Toast.LENGTH_SHORT).show();
finish();
});
}
gestureDetector = new GestureDetector(this, new GestureListener());
@Override
public void error(int errorCode, Void object) {
runOnUiThread(() -> Toast.makeText(StoryViewActivity.this, errorCode, Toast.LENGTH_SHORT).show());
}
viewPager.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
@Override
public void userInputRequired(android.app.PendingIntent pi, Void object) {
}
});
})
.setNegativeButton(R.string.cancel, null)
.create()
.show();
return true;
} else if (itemId == R.id.action_reply_to_story) {
if (storyMessage != null) {
Conversation conversation = xmppConnectionService.findOrCreateConversation(mAccount, contact, false, false);
conversation.setReplyTo(storyMessage);
switchToConversation(conversation);
StoryPagerAdapter adapter = new StoryPagerAdapter(this);
viewPager.setAdapter(adapter);
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
updateUiForPosition(position);
}
return true;
}
return super.onOptionsItemSelected(item);
});
updateUiForPosition(0);
}
@Override
public void onBackendConnected() {
String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT);
if (accountUuid != null) {
mAccount = xmppConnectionService.findAccountByUuid(accountUuid);
}
invalidateOptionsMenu();
loadStory();
}
private void loadStory() {
if (urls == null || currentIndex >= urls.size()) {
finish();
return;
}
titleView.setText(titles.get(currentIndex));
private void updateUiForPosition(int position) {
if (getSupportActionBar() != null) {
Contact storyContact = null;
if (contact != null && xmppConnectionService != null) {
@ -244,8 +153,8 @@ public class StoryViewActivity extends XmppActivity {
}
toolbarTitle.setText(displayName);
long publishedTimestamp = 0;
if (storyIds != null && currentIndex < storyIds.size()) {
final String currentStoryId = storyIds.get(currentIndex);
if (storyIds != null && position < storyIds.size()) {
final String currentStoryId = storyIds.get(position);
if (xmppConnectionService != null) {
for (eu.siacs.conversations.entities.Story story : xmppConnectionService.getStories()) {
if (story.getUuid().equals(currentStoryId)) {
@ -261,77 +170,9 @@ public class StoryViewActivity extends XmppActivity {
toolbarSubtitle.setText("");
}
}
progressView.setText((currentIndex + 1) + " " + getString(R.string.of) + " " + urls.size());
showSystemUi();
final String url = urls.get(currentIndex);
final File cacheFile = xmppConnectionService.getFileBackend().getStoryCacheFile(url);
final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(this);
circularProgressDrawable.setStrokeWidth(10f);
circularProgressDrawable.setCenterRadius(50f);
circularProgressDrawable.setColorSchemeColors(0xFFFFFFFF);
imageView.setImageDrawable(circularProgressDrawable);
videoView.setVisibility(View.GONE);
imageView.setVisibility(View.VISIBLE);
new Thread(() -> {
try {
if (!cacheFile.exists() || cacheFile.length() == 0) {
Log.d(Config.LOGTAG, "Story not in cache. Downloading from: " + url);
runOnUiThread(circularProgressDrawable::start);
final HttpUrl httpUrl = HttpUrl.get(url);
final boolean useTor = mAccount != null && (xmppConnectionService.useTorToConnect() || mAccount.isOnion());
final boolean useI2p = mAccount != null && (xmppConnectionService.useI2PToConnect() || mAccount.isI2P());
try (InputStream inputStream = HttpConnectionManager.open(httpUrl, useTor, useI2p);
FileOutputStream outputStream = new FileOutputStream(cacheFile)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
} else {
Log.d(Config.LOGTAG, "Loading story from cache: " + cacheFile.getName());
}
runOnUiThread(() -> {
circularProgressDrawable.stop();
if (!isFinishing()) {
String mimeType = (mimeTypes != null && currentIndex < mimeTypes.size()) ? mimeTypes.get(currentIndex) : null;
if (mimeType == null) {
mimeType = getContentResolver().getType(Uri.fromFile(cacheFile));
}
videoView.stopPlayback();
if (mimeType != null && mimeType.startsWith("video/")) {
imageView.setVisibility(View.GONE);
videoView.setVisibility(View.VISIBLE);
videoView.setVideoURI(Uri.fromFile(cacheFile));
videoView.setOnPreparedListener(mp -> {
mp.setLooping(true);
videoView.start();
});
} else {
videoView.setVisibility(View.GONE);
imageView.setVisibility(View.VISIBLE);
Glide.with(StoryViewActivity.this).load(cacheFile).into(imageView);
}
}
});
} catch (IOException e) {
Log.e(Config.LOGTAG, "Failed to download or load story", e);
if (cacheFile != null && cacheFile.exists()) {
cacheFile.delete();
}
runOnUiThread(() -> {
Toast.makeText(StoryViewActivity.this, R.string.download_failed_file_not_found, Toast.LENGTH_SHORT).show();
finish();
});
}
}).start();
titleView.setText(titles.get(position));
progressView.setText((position + 1) + " " + getString(R.string.of) + " " + urls.size());
}
private void hideSystemUi() {
@ -340,13 +181,6 @@ public class StoryViewActivity extends XmppActivity {
actionBar.hide();
}
bottomPanel.setVisibility(View.GONE);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN);
}
private void showSystemUi() {
@ -355,14 +189,102 @@ public class StoryViewActivity extends XmppActivity {
actionBar.show();
}
bottomPanel.setVisibility(View.VISIBLE);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
private boolean isSystemUiVisible() {
return (getWindow().getDecorView().getSystemUiVisibility() & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0;
ActionBar actionBar = getSupportActionBar();
return actionBar != null && actionBar.isShowing();
}
@Override
protected void refreshUiReal() {
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_story_view, menu);
MenuItem deleteButton = menu.findItem(R.id.action_delete_story);
if (contact != null && xmppConnectionService != null) {
final Account storyOwner = xmppConnectionService.findAccountByJid(contact);
if (storyOwner != null && storyOwner.isOnlineAndConnected()) {
deleteButton.setVisible(true);
this.mAccount = storyOwner;
}
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.action_delete_story) {
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.delete_story_dialog_title)
.setMessage(R.string.delete_story_dialog_message)
.setPositiveButton(R.string.delete, (dialog, which) -> {
xmppConnectionService.retractStory(mAccount, storyIds.get(viewPager.getCurrentItem()), new UiCallback<Void>() {
@Override
public void success(Void aVoid) {
runOnUiThread(() -> {
Toast.makeText(StoryViewActivity.this, R.string.story_deleted, Toast.LENGTH_SHORT).show();
finish();
});
}
@Override
public void error(int errorCode, Void object) {
runOnUiThread(() -> Toast.makeText(StoryViewActivity.this, errorCode, Toast.LENGTH_SHORT).show());
}
@Override
public void userInputRequired(android.app.PendingIntent pi, Void object) {
}
});
})
.setNegativeButton(R.string.cancel, null)
.create()
.show();
return true;
} else if (itemId == R.id.action_reply_to_story) {
int currentPos = viewPager.getCurrentItem();
Message storyMessage = new Message(null, titles.get(currentPos), Message.ENCRYPTION_NONE, Message.STATUS_RECEIVED);
Conversation conversation = xmppConnectionService.findOrCreateConversation(mAccount, contact, false, false);
conversation.setReplyTo(storyMessage);
switchToConversation(conversation);
return true;
}
return super.onOptionsItemSelected(item);
}
private class StoryPagerAdapter extends FragmentStateAdapter {
public StoryPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
@NonNull
@Override
public Fragment createFragment(int position) {
return StoryFragment.newInstance(urls.get(position), mimeTypes.get(position));
}
@Override
public int getItemCount() {
return urls.size();
}
}
@Override
protected void onBackendConnected() {
String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT);
if (accountUuid != null) {
mAccount = xmppConnectionService.findAccountByUuid(accountUuid);
}
invalidateOptionsMenu();
}
}

View file

@ -6,18 +6,10 @@
android:background="@android:color/black"
android:fitsSystemWindows="true">
<VideoView
android:id="@+id/story_video_view"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone" />
<ImageView
android:id="@+id/story_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter" />
android:layout_height="match_parent" />
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<VideoView
android:id="@+id/story_video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone" />
<ImageView
android:id="@+id/story_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter" />
</FrameLayout>