mirror of
https://github.com/nextcloud/android.git
synced 2024-11-27 17:46:37 +03:00
Merge pull request #11936 from nextcloud/migrate-to-media3
Migrate to media3 and Immersive mode for video playback
This commit is contained in:
commit
76369d4880
20 changed files with 1047 additions and 246 deletions
|
@ -299,8 +299,9 @@ dependencies {
|
|||
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
|
||||
implementation "com.google.android.exoplayer:exoplayer:$exoplayerVersion"
|
||||
implementation "com.google.android.exoplayer:extension-okhttp:$exoplayerVersion"
|
||||
implementation "androidx.media3:media3-ui:1.2.0"
|
||||
implementation "androidx.media3:media3-exoplayer:1.2.0"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:1.2.0"
|
||||
|
||||
implementation 'me.zhanghai.android.fastscroll:library:1.2.0'
|
||||
|
||||
|
|
|
@ -234,6 +234,11 @@
|
|||
android:name=".ui.preview.PreviewImageActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.ownCloud.Overlay" />
|
||||
<activity
|
||||
android:name=".ui.preview.PreviewMediaActivity"
|
||||
android:exported="false"
|
||||
android:configChanges="orientation|screenLayout|screenSize|keyboardHidden"
|
||||
android:theme="@style/Theme.ownCloud.Media" />
|
||||
<service
|
||||
android:name=".authentication.AccountAuthenticatorService"
|
||||
android:exported="false">
|
||||
|
|
|
@ -127,6 +127,7 @@ import com.owncloud.android.ui.preview.FileDownloadFragment;
|
|||
import com.owncloud.android.ui.preview.PreviewBitmapActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewImageActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewImageFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewTextFileFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewTextFragment;
|
||||
|
@ -206,6 +207,9 @@ abstract class ComponentsModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract PreviewImageActivity previewImageActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewMediaActivity previewMediaActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ReceiveExternalFilesActivity receiveExternalFilesActivity();
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ package com.nextcloud.client.media
|
|||
|
||||
import android.content.Context
|
||||
import android.media.MediaPlayer
|
||||
import com.google.android.exoplayer2.PlaybackException
|
||||
import androidx.media3.common.PlaybackException
|
||||
import com.owncloud.android.R
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,9 +25,9 @@ package com.nextcloud.client.media
|
|||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.view.View
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.PlaybackException
|
||||
import com.google.android.exoplayer2.Player
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
|
|
|
@ -22,10 +22,12 @@
|
|||
package com.nextcloud.client.media
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import com.nextcloud.common.NextcloudClient
|
||||
import com.owncloud.android.MainApp
|
||||
|
||||
|
@ -37,6 +39,7 @@ object NextcloudExoPlayer {
|
|||
* IP versions and certificates.
|
||||
*
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
@JvmStatic
|
||||
fun createNextcloudExoplayer(context: Context, nextcloudClient: NextcloudClient): ExoPlayer {
|
||||
val okHttpDataSourceFactory = OkHttpDataSource.Factory(nextcloudClient.client)
|
||||
|
|
|
@ -123,6 +123,7 @@ import com.owncloud.android.ui.helpers.FileOperationsHelper;
|
|||
import com.owncloud.android.ui.helpers.UriUploader;
|
||||
import com.owncloud.android.ui.preview.PreviewImageActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewImageFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewTextFileFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewTextFragment;
|
||||
|
@ -168,7 +169,10 @@ import static com.owncloud.android.utils.PermissionUtil.PERMISSION_CHOICE_DIALOG
|
|||
/**
|
||||
* Displays, what files the user has available in his ownCloud. This is the main view.
|
||||
*/
|
||||
public class FileDisplayActivity extends FileActivity implements FileFragment.ContainerActivity, OnEnforceableRefreshListener, SortingOrderDialogFragment.OnSortingOrderListener, SendShareDialog.SendShareDialogDownloader, Injectable {
|
||||
public class FileDisplayActivity extends FileActivity
|
||||
implements FileFragment.ContainerActivity,
|
||||
OnEnforceableRefreshListener, SortingOrderDialogFragment.OnSortingOrderListener,
|
||||
SendShareDialog.SendShareDialogDownloader, Injectable {
|
||||
|
||||
public static final String RESTART = "RESTART";
|
||||
public static final String ALL_FILES = "ALL_FILES";
|
||||
|
@ -693,8 +697,8 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
|
|||
// update the file from database, for the local storage path
|
||||
mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId());
|
||||
|
||||
if (PreviewMediaFragment.canBePreviewed(mWaitingToPreview)) {
|
||||
startMediaPreview(mWaitingToPreview, 0, true, true, true);
|
||||
if (PreviewMediaActivity.Companion.canBePreviewed(mWaitingToPreview)) {
|
||||
startMediaPreview(mWaitingToPreview, 0, true, true, true, true);
|
||||
detailsFragmentChanged = true;
|
||||
} else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimeType())) {
|
||||
startContactListFragment(mWaitingToPreview);
|
||||
|
@ -1391,7 +1395,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
|
|||
if (uploadWasFine) {
|
||||
OCFile ocFile = getFile();
|
||||
if (PreviewImageFragment.canBePreviewed(ocFile)) {
|
||||
startImagePreview(getFile(),true);
|
||||
startImagePreview(getFile(), true);
|
||||
} else if (PreviewTextFileFragment.canBePreviewed(ocFile)) {
|
||||
startTextPreview(ocFile, true);
|
||||
}
|
||||
|
@ -1643,10 +1647,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
|
|||
OCFile file = ((FileFragment) details).getFile();
|
||||
if (file != null) {
|
||||
file = getStorageManager().getFileByPath(file.getRemotePath());
|
||||
if (details instanceof PreviewMediaFragment) {
|
||||
// Refresh OCFile of the fragment
|
||||
((PreviewMediaFragment) details).updateFile(file);
|
||||
} else if (details instanceof PreviewTextFragment) {
|
||||
if (details instanceof PreviewTextFragment) {
|
||||
// Refresh OCFile of the fragment
|
||||
((PreviewTextFileFragment) details).updateFile(file);
|
||||
} else {
|
||||
|
@ -1676,12 +1677,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
|
|||
|
||||
// check if file is still available, if so do nothing
|
||||
boolean fileAvailable = getStorageManager().fileExists(removedFile.getFileId());
|
||||
|
||||
if (leftFragment instanceof FileFragment && !fileAvailable && removedFile.equals(((FileFragment) leftFragment).getFile())) {
|
||||
if (leftFragment instanceof PreviewMediaFragment previewMediaFragment) {
|
||||
previewMediaFragment.stopPreview(true);
|
||||
onBackPressed();
|
||||
}
|
||||
setFile(getStorageManager().getFileById(removedFile.getParentId()));
|
||||
resetTitleBarAndScrolling();
|
||||
}
|
||||
|
@ -1796,7 +1792,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
|
|||
((PreviewMediaFragment) fileFragment).updateFile(renamedFile);
|
||||
if (PreviewMediaFragment.canBePreviewed(renamedFile)) {
|
||||
long position = ((PreviewMediaFragment) fileFragment).getPosition();
|
||||
startMediaPreview(renamedFile, position, true, true, true);
|
||||
startMediaPreview(renamedFile, position, true, true, true, false);
|
||||
} else {
|
||||
getFileOperationsHelper().openFile(renamedFile);
|
||||
}
|
||||
|
@ -2041,15 +2037,19 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
|
|||
* @param startPlaybackPosition Media position where the playback will be started, in milliseconds.
|
||||
* @param autoplay When 'true', the playback will start without user interactions.
|
||||
*/
|
||||
public void startMediaPreview(OCFile file, long startPlaybackPosition, boolean autoplay, boolean showPreview, boolean streamMedia) {
|
||||
public void startMediaPreview(OCFile file, long startPlaybackPosition, boolean autoplay, boolean showPreview, boolean streamMedia, boolean showInActivity) {
|
||||
Optional<User> user = getUser();
|
||||
if (!user.isPresent()) {
|
||||
return; // not reachable under normal conditions
|
||||
}
|
||||
if (showPreview && file.isDown() && !file.isDownloading() || streamMedia) {
|
||||
configureToolbarForPreview(file);
|
||||
Fragment mediaFragment = PreviewMediaFragment.newInstance(file, user.get(), startPlaybackPosition, autoplay, false);
|
||||
setLeftFragment(mediaFragment, false);
|
||||
if (showInActivity) {
|
||||
startMediaActivity(file, startPlaybackPosition, autoplay, user);
|
||||
} else {
|
||||
configureToolbarForPreview(file);
|
||||
Fragment mediaFragment = PreviewMediaFragment.newInstance(file, user.get(), startPlaybackPosition, autoplay, false);
|
||||
setLeftFragment(mediaFragment, false);
|
||||
}
|
||||
} else {
|
||||
Intent previewIntent = new Intent();
|
||||
previewIntent.putExtra(EXTRA_FILE, file);
|
||||
|
@ -2060,6 +2060,15 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
|
|||
}
|
||||
}
|
||||
|
||||
private void startMediaActivity(OCFile file, long startPlaybackPosition, boolean autoplay, Optional<User> user) {
|
||||
Intent previewMediaIntent = new Intent(this, PreviewMediaActivity.class);
|
||||
previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_FILE, file);
|
||||
previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_USER, user.get());
|
||||
previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_START_POSITION, startPlaybackPosition);
|
||||
previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_AUTOPLAY, autoplay);
|
||||
startActivity(previewMediaIntent);
|
||||
}
|
||||
|
||||
public void configureToolbarForPreview(OCFile file) {
|
||||
lockScrolling();
|
||||
super.updateActionBarTitleAndHomeButton(file);
|
||||
|
@ -2242,7 +2251,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
|
|||
if (event.getIntent().getBooleanExtra(TEXT_PREVIEW, false)) {
|
||||
startTextPreview((OCFile) bundle.get(EXTRA_FILE), true);
|
||||
} else if (bundle.containsKey(PreviewMediaFragment.EXTRA_START_POSITION)) {
|
||||
startMediaPreview((OCFile) bundle.get(EXTRA_FILE), (long) bundle.get(PreviewMediaFragment.EXTRA_START_POSITION), (boolean) bundle.get(PreviewMediaFragment.EXTRA_AUTOPLAY), true, true);
|
||||
startMediaPreview((OCFile) bundle.get(EXTRA_FILE), (long) bundle.get(PreviewMediaFragment.EXTRA_START_POSITION), (boolean) bundle.get(PreviewMediaFragment.EXTRA_AUTOPLAY), true, true, true);
|
||||
} else if (bundle.containsKey(PreviewImageActivity.EXTRA_VIRTUAL_TYPE)) {
|
||||
startImagePreview((OCFile) bundle.get(EXTRA_FILE), (VirtualFolderType) bundle.get(PreviewImageActivity.EXTRA_VIRTUAL_TYPE), true);
|
||||
} else {
|
||||
|
|
|
@ -155,6 +155,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
|
|||
dialog.dismiss()
|
||||
notifyResult()
|
||||
}
|
||||
|
||||
KEY_EXISTING_USED -> {
|
||||
decryptPrivateKey(dialog)
|
||||
}
|
||||
|
@ -162,6 +163,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
|
|||
KEY_GENERATE -> {
|
||||
generateKey()
|
||||
}
|
||||
|
||||
else -> dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -376,7 +378,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
|
|||
binding.encryptionStatus.setText(R.string.end_to_end_encryption_generating_keys)
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown", "ReturnCount")
|
||||
@Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown", "ReturnCount", "LongMethod")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun doInBackground(vararg voids: Void?): String {
|
||||
// - create CSR, push to server, store returned public key in database
|
||||
|
|
|
@ -112,7 +112,7 @@ import com.owncloud.android.ui.events.SearchEvent;
|
|||
import com.owncloud.android.ui.helpers.FileOperationsHelper;
|
||||
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
|
||||
import com.owncloud.android.ui.preview.PreviewImageFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaFragment;
|
||||
import com.owncloud.android.ui.preview.PreviewMediaActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewTextFileFragment;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.EncryptionUtils;
|
||||
|
@ -139,6 +139,7 @@ import javax.inject.Inject;
|
|||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
@ -146,6 +147,7 @@ import androidx.core.content.ContextCompat;
|
|||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
@ -977,6 +979,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public void onItemClicked(OCFile file) {
|
||||
((FileActivity) mContainerActivity).checkInternetConnection();
|
||||
|
||||
|
@ -1068,10 +1071,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
setFabVisible(false);
|
||||
((FileDisplayActivity) mContainerActivity).startTextPreview(file, false);
|
||||
} else if (file.isDown()) {
|
||||
if (PreviewMediaFragment.canBePreviewed(file)) {
|
||||
if (PreviewMediaActivity.Companion.canBePreviewed(file)) {
|
||||
// media preview
|
||||
setFabVisible(false);
|
||||
((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, false);
|
||||
((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, false, true);
|
||||
} else {
|
||||
mContainerActivity.getFileOperationsHelper().openFile(file);
|
||||
}
|
||||
|
@ -1081,10 +1084,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
OCCapability capability = mContainerActivity.getStorageManager()
|
||||
.getCapability(account.getAccountName());
|
||||
|
||||
if (PreviewMediaFragment.canBePreviewed(file) && !file.isEncrypted()) {
|
||||
if (PreviewMediaActivity.Companion.canBePreviewed(file) && !file.isEncrypted()) {
|
||||
// stream media preview on >= NC14
|
||||
setFabVisible(false);
|
||||
((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true);
|
||||
((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true, true);
|
||||
} else if (editorUtils.isEditorAvailable(accountManager.getUser(),
|
||||
file.getMimeType()) &&
|
||||
!file.isEncrypted()) {
|
||||
|
@ -1822,8 +1825,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
}
|
||||
|
||||
/**
|
||||
* Theme default action bar according to provided parameters.
|
||||
* Replaces back arrow with hamburger menu icon.
|
||||
* Theme default action bar according to provided parameters. Replaces back arrow with hamburger menu icon.
|
||||
*
|
||||
* @param title string res id of title to be shown in action bar
|
||||
*/
|
||||
|
@ -1834,7 +1836,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
|
|||
/**
|
||||
* Theme default action bar according to provided parameters.
|
||||
*
|
||||
* @param title title to be shown in action bar
|
||||
* @param title title to be shown in action bar
|
||||
* @param showBackAsMenu iff true replace back arrow with hamburger menu icon
|
||||
*/
|
||||
protected void setTitle(final String title, Boolean showBackAsMenu) {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
*/
|
||||
package com.owncloud.android.ui.preview;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.util.SparseArray;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
|
@ -38,9 +39,11 @@ import java.util.Set;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/**
|
||||
* Adapter class that provides Fragment instances
|
||||
|
@ -59,10 +62,10 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
|
|||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param fragmentManager {@link FragmentManager} instance that will handle
|
||||
* the {@link Fragment}s provided by the adapter.
|
||||
* @param parentFolder Folder where images will be searched for.
|
||||
* @param storageManager Bridge to database.
|
||||
* @param fragmentManager {@link FragmentManager} instance that will handle the {@link Fragment}s provided by the
|
||||
* adapter.
|
||||
* @param parentFolder Folder where images will be searched for.
|
||||
* @param storageManager Bridge to database.
|
||||
*/
|
||||
public PreviewImagePagerAdapter(FragmentManager fragmentManager,
|
||||
OCFile selectedFile,
|
||||
|
@ -96,8 +99,8 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
|
|||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param fragmentManager {@link FragmentManager} instance that will handle
|
||||
* the {@link Fragment}s provided by the adapter.
|
||||
* @param fragmentManager {@link FragmentManager} instance that will handle the {@link Fragment}s provided by the
|
||||
* adapter.
|
||||
* @param type Type of virtual folder, e.g. favorite or photos
|
||||
* @param storageManager Bridge to database.
|
||||
*/
|
||||
|
@ -113,7 +116,7 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
|
|||
if (type == null) {
|
||||
throw new IllegalArgumentException("NULL parent folder");
|
||||
}
|
||||
if(type == VirtualFolderType.NONE){
|
||||
if (type == VirtualFolderType.NONE) {
|
||||
throw new IllegalArgumentException("NONE virtual folder type");
|
||||
}
|
||||
if (storageManager == null) {
|
||||
|
@ -155,6 +158,7 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public Fragment getItem(int i) {
|
||||
OCFile file = getFileAt(i);
|
||||
Fragment fragment;
|
||||
|
@ -242,7 +246,7 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
|
|||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
mCachedFragments.remove(position);
|
||||
super.destroyItem(container, position, object);
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,834 @@
|
|||
/*
|
||||
* ownCloud Android client application
|
||||
*
|
||||
* @author David A. Velasco
|
||||
* @author Chris Narkiewicz
|
||||
* @author Andy Scherzinger
|
||||
* @author TSI-mc
|
||||
* @author Parneet Singh
|
||||
* Copyright (C) 2016 ownCloud Inc.
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* Copyright (C) 2020 Andy Scherzinger
|
||||
* Copyright (C) 2023 TSI-mc
|
||||
* Copyright (C) Parneet Singh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.owncloud.android.ui.preview
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.nextcloud.client.media.ExoplayerListener
|
||||
import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
|
||||
import com.nextcloud.client.media.PlayerServiceConnection
|
||||
import com.nextcloud.client.network.ClientFactory
|
||||
import com.nextcloud.client.network.ClientFactory.CreationException
|
||||
import com.nextcloud.common.NextcloudClient
|
||||
import com.nextcloud.ui.fileactions.FileActionsBottomSheet.Companion.newInstance
|
||||
import com.nextcloud.ui.fileactions.FileActionsBottomSheet.ResultListener
|
||||
import com.nextcloud.utils.extensions.getParcelableArgument
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.ActivityPreviewMediaBinding
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
||||
import com.owncloud.android.files.StreamMediaFileOperation
|
||||
import com.owncloud.android.files.services.FileDownloader
|
||||
import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.operations.OnRemoteOperationListener
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.RemoveFileOperation
|
||||
import com.owncloud.android.operations.SynchronizeFileOperation
|
||||
import com.owncloud.android.ui.activity.FileActivity
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity
|
||||
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment
|
||||
import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment
|
||||
import com.owncloud.android.ui.dialog.SendShareDialog
|
||||
import com.owncloud.android.ui.fragment.FileFragment
|
||||
import com.owncloud.android.ui.fragment.OCFileListFragment
|
||||
import com.owncloud.android.utils.BitmapUtils
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.ErrorMessageAdapter
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This activity shows a preview of a downloaded media file (audio or video).
|
||||
*
|
||||
*
|
||||
* Trying to get an instance with NULL [OCFile] or ownCloud [User] values will produce an [ ].
|
||||
*
|
||||
*
|
||||
* By now, if the [OCFile] passed is not downloaded, an [IllegalStateException] is generated on
|
||||
* instantiation too.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class PreviewMediaActivity :
|
||||
FileActivity(),
|
||||
FileFragment.ContainerActivity,
|
||||
OnRemoteOperationListener,
|
||||
SendShareDialog.SendShareDialogDownloader,
|
||||
Injectable {
|
||||
|
||||
private var user: User? = null
|
||||
private var savedPlaybackPosition: Long = 0
|
||||
private var autoplay = true
|
||||
private val prepared = false
|
||||
private var mediaPlayerServiceConnection: PlayerServiceConnection? = null
|
||||
private var videoUri: Uri? = null
|
||||
|
||||
@Inject
|
||||
lateinit var clientFactory: ClientFactory
|
||||
|
||||
@Inject
|
||||
lateinit var accountManager: UserAccountManager
|
||||
|
||||
@Inject
|
||||
lateinit var backgroundJobManager: BackgroundJobManager
|
||||
|
||||
private lateinit var binding: ActivityPreviewMediaBinding
|
||||
private var emptyListView: ViewGroup? = null
|
||||
private var exoPlayer: ExoPlayer? = null
|
||||
private var nextcloudClient: NextcloudClient? = null
|
||||
private lateinit var windowInsetsController: WindowInsetsControllerCompat
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPreviewMediaBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.materialToolbar)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
applyWindowInsets()
|
||||
initArguments(savedInstanceState)
|
||||
mediaPlayerServiceConnection = PlayerServiceConnection(this)
|
||||
showMediaTypeViews()
|
||||
configureSystemBars()
|
||||
emptyListView = binding.emptyView.emptyListView
|
||||
setLoadingView()
|
||||
}
|
||||
|
||||
private fun initArguments(savedInstanceState: Bundle?) {
|
||||
intent?.let {
|
||||
initWithIntent(it)
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
checkNotNull(file) { "Instanced with a NULL OCFile" }
|
||||
checkNotNull(user) { "Instanced with a NULL ownCloud Account" }
|
||||
} else {
|
||||
initWithBundle(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initWithIntent(intent: Intent) {
|
||||
file = intent.getParcelableArgument(FILE, OCFile::class.java)
|
||||
user = intent.getParcelableArgument(USER, User::class.java)
|
||||
savedPlaybackPosition = intent.getLongExtra(PLAYBACK_POSITION, 0L)
|
||||
autoplay = intent.getBooleanExtra(AUTOPLAY, true)
|
||||
}
|
||||
|
||||
private fun initWithBundle(bundle: Bundle) {
|
||||
file = bundle.getParcelableArgument(EXTRA_FILE, OCFile::class.java)
|
||||
user = bundle.getParcelableArgument(EXTRA_USER, User::class.java)
|
||||
savedPlaybackPosition = bundle.getInt(EXTRA_PLAY_POSITION).toLong()
|
||||
autoplay = bundle.getBoolean(EXTRA_PLAYING)
|
||||
}
|
||||
|
||||
private fun showMediaTypeViews() {
|
||||
if (file == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val isFileVideo = MimeTypeUtil.isVideo(file)
|
||||
|
||||
binding.exoplayerView.visibility = if (isFileVideo) View.VISIBLE else View.GONE
|
||||
binding.imagePreview.visibility = if (isFileVideo) View.GONE else View.VISIBLE
|
||||
|
||||
if (isFileVideo) {
|
||||
binding.root.setBackgroundColor(resources.getColor(R.color.black, null))
|
||||
} else {
|
||||
extractAndSetCoverArt(file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureSystemBars() {
|
||||
updateActionBarTitleAndHomeButton(file)
|
||||
|
||||
supportActionBar?.let {
|
||||
it.setDisplayHomeAsUpEnabled(true)
|
||||
viewThemeUtils.files.themeActionBar(this, it)
|
||||
}
|
||||
|
||||
viewThemeUtils.platform.themeStatusBar(
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
private fun setLoadingView() {
|
||||
binding.progress.visibility = View.VISIBLE
|
||||
binding.emptyView.emptyListView.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun setVideoErrorMessage(headline: String, @StringRes message: Int) {
|
||||
binding.emptyView.run {
|
||||
emptyListViewHeadline.text = headline
|
||||
emptyListViewText.setText(message)
|
||||
emptyListIcon.setImageResource(R.drawable.file_movie)
|
||||
emptyListViewText.visibility = View.VISIBLE
|
||||
emptyListIcon.visibility = View.VISIBLE
|
||||
binding.progress.visibility = View.GONE
|
||||
emptyListView.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tries to read the cover art from the audio file and sets it as cover art.
|
||||
*
|
||||
* @param file audio file with potential cover art
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught", "NestedBlockDepth")
|
||||
private fun extractAndSetCoverArt(file: OCFile) {
|
||||
if (!MimeTypeUtil.isAudio(file)) {
|
||||
return
|
||||
}
|
||||
|
||||
val bitmap = if (file.storagePath == null) {
|
||||
getAudioThumbnail(file)
|
||||
} else {
|
||||
getThumbnail(file.storagePath) ?: getAudioThumbnail(file)
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
binding.imagePreview.setImageBitmap(bitmap)
|
||||
} else {
|
||||
setGenericThumbnail()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun getThumbnail(storagePath: String?): Bitmap? {
|
||||
return try {
|
||||
MediaMetadataRetriever().run {
|
||||
setDataSource(storagePath)
|
||||
BitmapFactory.decodeByteArray(embeddedPicture, 0, embeddedPicture?.size ?: 0)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
BitmapUtils.drawableToBitmap(genericThumbnail())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAudioThumbnail(file: OCFile): Bitmap? {
|
||||
return ThumbnailsCacheManager.getBitmapFromDiskCache(
|
||||
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
|
||||
)
|
||||
}
|
||||
|
||||
private fun setGenericThumbnail() {
|
||||
binding.imagePreview.setImageDrawable(genericThumbnail())
|
||||
}
|
||||
|
||||
private fun genericThumbnail(): Drawable? {
|
||||
val result = AppCompatResources.getDrawable(this, R.drawable.logo)
|
||||
result?.let {
|
||||
if (!resources.getBoolean(R.bool.is_branded_client)) {
|
||||
DrawableCompat.setTint(it, resources.getColor(R.color.primary, this.theme))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Log_OC.v(TAG, "onSaveInstanceState")
|
||||
outState.let { bundle ->
|
||||
bundle.putParcelable(EXTRA_FILE, file)
|
||||
bundle.putParcelable(EXTRA_USER, user)
|
||||
saveMediaInstanceState(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveMediaInstanceState(bundle: Bundle) {
|
||||
bundle.run {
|
||||
if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
|
||||
exoPlayer?.let {
|
||||
savedPlaybackPosition = it.currentPosition
|
||||
autoplay = it.isPlaying
|
||||
}
|
||||
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
|
||||
putBoolean(EXTRA_PLAYING, autoplay)
|
||||
} else if (mediaPlayerServiceConnection != null && mediaPlayerServiceConnection!!.isConnected) {
|
||||
putInt(EXTRA_PLAY_POSITION, mediaPlayerServiceConnection!!.currentPosition)
|
||||
putBoolean(EXTRA_PLAYING, mediaPlayerServiceConnection!!.isPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
Log_OC.v(TAG, "onStart")
|
||||
|
||||
if (file != null) {
|
||||
mediaPlayerServiceConnection?.bind()
|
||||
|
||||
if (MimeTypeUtil.isAudio(file)) {
|
||||
setupAudioPlayerServiceConnection()
|
||||
} else if (MimeTypeUtil.isVideo(file)) {
|
||||
if (mediaPlayerServiceConnection?.isConnected == true) {
|
||||
stopAudio()
|
||||
}
|
||||
|
||||
if (exoPlayer != null) {
|
||||
playVideo()
|
||||
} else {
|
||||
initNextcloudExoPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAudioPlayerServiceConnection() {
|
||||
binding.mediaController.run {
|
||||
setMediaPlayer(mediaPlayerServiceConnection)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
user?.let {
|
||||
mediaPlayerServiceConnection?.start(it, file, autoplay, savedPlaybackPosition)
|
||||
}
|
||||
|
||||
binding.emptyView.emptyListView.visibility = View.GONE
|
||||
binding.progress.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun initNextcloudExoPlayer() {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
Executors.newSingleThreadExecutor().execute {
|
||||
try {
|
||||
nextcloudClient = clientFactory.createNextcloudClient(accountManager.user)
|
||||
|
||||
nextcloudClient?.let { client ->
|
||||
handler.post {
|
||||
exoPlayer = createNextcloudExoplayer(this, client)
|
||||
|
||||
exoPlayer?.let { player ->
|
||||
player.addListener(
|
||||
ExoplayerListener(
|
||||
this,
|
||||
binding.exoplayerView,
|
||||
player
|
||||
)
|
||||
)
|
||||
|
||||
playVideo()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: CreationException) {
|
||||
handler.post { Log_OC.e(TAG, "error setting up ExoPlayer", e) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initWindowInsetsController() {
|
||||
windowInsetsController = WindowCompat.getInsetsController(
|
||||
window,
|
||||
window.decorView
|
||||
).apply {
|
||||
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyWindowInsets() {
|
||||
val playerView = binding.exoplayerView
|
||||
val exoControls = playerView.findViewById<FrameLayout>(R.id.exo_bottom_bar)
|
||||
val exoProgress = playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||
val progressBottomMargin = exoProgress.marginBottom
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
|
||||
val insets = windowInsets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type
|
||||
.displayCutout()
|
||||
)
|
||||
|
||||
binding.materialToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
exoControls.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom
|
||||
}
|
||||
exoProgress.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom + progressBottomMargin
|
||||
}
|
||||
exoControls.updatePadding(left = insets.left, right = insets.right)
|
||||
exoProgress.updatePadding(left = insets.left, right = insets.right)
|
||||
binding.materialToolbar.updatePadding(left = insets.left, right = insets.right)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun setupVideoView() {
|
||||
initWindowInsetsController()
|
||||
val type = WindowInsetsCompat.Type.systemBars()
|
||||
binding.exoplayerView.let {
|
||||
it.setShowNextButton(false)
|
||||
it.setShowPreviousButton(false)
|
||||
it.setControllerVisibilityListener(
|
||||
PlayerView.ControllerVisibilityListener { visibility ->
|
||||
if (visibility == View.VISIBLE) {
|
||||
windowInsetsController.show(type)
|
||||
supportActionBar!!.show()
|
||||
} else if (visibility == View.GONE) {
|
||||
windowInsetsController.hide(type)
|
||||
supportActionBar!!.hide()
|
||||
}
|
||||
}
|
||||
)
|
||||
it.player = exoPlayer
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopAudio() {
|
||||
mediaPlayerServiceConnection?.stop()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
if (item.itemId == R.id.custom_menu_placeholder_item) {
|
||||
val file = file
|
||||
|
||||
if (storageManager != null && file != null) {
|
||||
val updatedFile = storageManager.getFileById(file.fileId)
|
||||
setFile(updatedFile)
|
||||
val fileNew = getFile()
|
||||
fileNew?.let { showFileActions(it) }
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun showFileActions(file: OCFile) {
|
||||
val additionalFilter: MutableList<Int> =
|
||||
mutableListOf(
|
||||
R.id.action_rename_file,
|
||||
R.id.action_sync_file,
|
||||
R.id.action_move_or_copy,
|
||||
R.id.action_favorite,
|
||||
R.id.action_unset_favorite,
|
||||
R.id.action_pin_to_homescreen
|
||||
)
|
||||
|
||||
if (getFile() != null && getFile().isSharedWithMe && !getFile().canReshare()) {
|
||||
additionalFilter.add(R.id.action_send_share_file)
|
||||
}
|
||||
|
||||
newInstance(file, false, additionalFilter)
|
||||
.setResultListener(
|
||||
supportFragmentManager,
|
||||
this,
|
||||
object : ResultListener {
|
||||
override fun onResult(actionId: Int) {
|
||||
onFileActionChosen(actionId)
|
||||
}
|
||||
}
|
||||
)
|
||||
.show(supportFragmentManager, "actions")
|
||||
}
|
||||
|
||||
private fun onFileActionChosen(itemId: Int) {
|
||||
when (itemId) {
|
||||
R.id.action_send_share_file -> {
|
||||
sendShareFile()
|
||||
}
|
||||
|
||||
R.id.action_open_file_with -> {
|
||||
openFile()
|
||||
}
|
||||
|
||||
R.id.action_remove_file -> {
|
||||
val dialog = RemoveFilesDialogFragment.newInstance(file)
|
||||
dialog.show(supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)
|
||||
}
|
||||
|
||||
R.id.action_see_details -> {
|
||||
seeDetails()
|
||||
}
|
||||
|
||||
R.id.action_sync_file -> {
|
||||
fileOperationsHelper.syncFile(file)
|
||||
}
|
||||
|
||||
R.id.action_cancel_sync -> {
|
||||
fileOperationsHelper.cancelTransference(file)
|
||||
}
|
||||
|
||||
R.id.action_stream_media -> {
|
||||
fileOperationsHelper.streamMediaFile(file)
|
||||
}
|
||||
|
||||
R.id.action_export_file -> {
|
||||
val list = ArrayList<OCFile>()
|
||||
list.add(file)
|
||||
fileOperationsHelper.exportFiles(
|
||||
list,
|
||||
this,
|
||||
binding.root,
|
||||
backgroundJobManager
|
||||
)
|
||||
}
|
||||
|
||||
R.id.action_download_file -> {
|
||||
requestForDownload(file, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>?) {
|
||||
super.onRemoteOperationFinish(operation, result)
|
||||
if (operation is RemoveFileOperation) {
|
||||
DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources))
|
||||
|
||||
val removedFile = operation.file
|
||||
val fileAvailable: Boolean = storageManager.fileExists(removedFile.fileId)
|
||||
if (!fileAvailable && removedFile == file) {
|
||||
finish()
|
||||
}
|
||||
} else if (operation is SynchronizeFileOperation) {
|
||||
onSynchronizeFileOperationFinish(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun newTransferenceServiceConnection(): ServiceConnection {
|
||||
return PreviewMediaServiceConnection()
|
||||
}
|
||||
|
||||
private fun onSynchronizeFileOperationFinish(result: RemoteOperationResult<*>?) {
|
||||
result?.let {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PreviewMediaServiceConnection : ServiceConnection {
|
||||
override fun onServiceConnected(componentName: ComponentName?, service: IBinder?) {
|
||||
componentName?.let {
|
||||
if (it == ComponentName(this@PreviewMediaActivity, FileDownloader::class.java)) {
|
||||
mDownloaderBinder = service as FileDownloaderBinder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(componentName: ComponentName?) {
|
||||
if (componentName == ComponentName(this@PreviewMediaActivity, FileDownloader::class.java)) {
|
||||
Log_OC.d(PreviewImageActivity.TAG, "Download service suddenly disconnected")
|
||||
mDownloaderBinder = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun downloadFile(file: OCFile?, packageName: String?, activityName: String?) {
|
||||
requestForDownload(file, OCFileListFragment.DOWNLOAD_SEND, packageName, activityName)
|
||||
}
|
||||
|
||||
private fun requestForDownload(
|
||||
file: OCFile?,
|
||||
downloadBehavior: String? = null,
|
||||
packageName: String? = null,
|
||||
activityName: String? = null
|
||||
) {
|
||||
if (fileDownloaderBinder.isDownloading(user, file)) {
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(this, FileDownloader::class.java).apply {
|
||||
putExtra(FileDownloader.EXTRA_USER, user)
|
||||
putExtra(FileDownloader.EXTRA_FILE, file)
|
||||
downloadBehavior?.let { behavior ->
|
||||
putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, behavior)
|
||||
}
|
||||
putExtra(SendShareDialog.PACKAGE_NAME, packageName)
|
||||
putExtra(SendShareDialog.ACTIVITY_NAME, activityName)
|
||||
}
|
||||
|
||||
startService(intent)
|
||||
}
|
||||
|
||||
private fun seeDetails() {
|
||||
stopPreview(false)
|
||||
showDetails(file)
|
||||
}
|
||||
|
||||
private fun sendShareFile() {
|
||||
stopPreview(false)
|
||||
fileOperationsHelper.sendShareFile(file)
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun playVideo() {
|
||||
setupVideoView()
|
||||
|
||||
if (file.isDown) {
|
||||
playVideoUri(file.storageUri)
|
||||
} else {
|
||||
try {
|
||||
LoadStreamUrl(this, user, clientFactory).execute(file.localId)
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Loading stream url not possible: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun playVideoUri(uri: Uri) {
|
||||
binding.progress.visibility = View.GONE
|
||||
|
||||
exoPlayer?.run {
|
||||
setMediaItem(MediaItem.fromUri(uri))
|
||||
playWhenReady = autoplay
|
||||
prepare()
|
||||
|
||||
if (savedPlaybackPosition >= 0) {
|
||||
seekTo(savedPlaybackPosition)
|
||||
}
|
||||
}
|
||||
|
||||
autoplay = false
|
||||
}
|
||||
|
||||
private class LoadStreamUrl(
|
||||
previewMediaActivity: PreviewMediaActivity,
|
||||
private val user: User?,
|
||||
private val clientFactory: ClientFactory?
|
||||
) : AsyncTask<Long?, Void?, Uri?>() {
|
||||
private val previewMediaActivityWeakReference: WeakReference<PreviewMediaActivity> =
|
||||
WeakReference(previewMediaActivity)
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun doInBackground(vararg fileId: Long?): Uri? {
|
||||
val client: OwnCloudClient? = try {
|
||||
clientFactory?.create(user)
|
||||
} catch (e: CreationException) {
|
||||
Log_OC.e(TAG, "Loading stream url not possible: $e")
|
||||
return null
|
||||
}
|
||||
|
||||
val sfo = StreamMediaFileOperation(fileId[0]!!)
|
||||
val result = sfo.execute(client)
|
||||
|
||||
return if (!result.isSuccess) {
|
||||
null
|
||||
} else {
|
||||
Uri.parse(result.data[0] as String)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onPostExecute(uri: Uri?) {
|
||||
val weakReference = previewMediaActivityWeakReference.get()
|
||||
weakReference?.apply {
|
||||
if (uri != null) {
|
||||
videoUri = uri
|
||||
playVideoUri(uri)
|
||||
} else {
|
||||
emptyListView?.visibility = View.VISIBLE
|
||||
setVideoErrorMessage(
|
||||
weakReference.getString(R.string.stream_not_possible_headline),
|
||||
R.string.stream_not_possible_message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
Log_OC.v(TAG, "onPause")
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
Log_OC.v(TAG, "onResume")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log_OC.v(TAG, "onDestroy")
|
||||
|
||||
super.onDestroy()
|
||||
|
||||
exoPlayer?.run {
|
||||
stop()
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log_OC.v(TAG, "onStop")
|
||||
|
||||
val file = file
|
||||
if (MimeTypeUtil.isAudio(file) && mediaPlayerServiceConnection?.isPlaying == false) {
|
||||
stopAudio()
|
||||
} else if (MimeTypeUtil.isVideo(file) && exoPlayer != null && exoPlayer?.isPlaying == true) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
|
||||
exoPlayer?.pause()
|
||||
}
|
||||
|
||||
mediaPlayerServiceConnection?.unbind()
|
||||
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun showDetails(file: OCFile?) {
|
||||
val intent = Intent(this, FileDisplayActivity::class.java).apply {
|
||||
action = FileDisplayActivity.ACTION_DETAILS
|
||||
putExtra(FileActivity.EXTRA_FILE, file)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun showDetails(file: OCFile?, activeTab: Int) {
|
||||
showDetails(file)
|
||||
}
|
||||
|
||||
override fun onBrowsedDownTo(folder: OCFile?) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
override fun onTransferStateChanged(file: OCFile?, downloading: Boolean, uploading: Boolean) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
Log_OC.v(TAG, "onConfigurationChanged $this")
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
Log_OC.v(TAG, "onActivityResult $this")
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
savedPlaybackPosition = data?.getLongExtra(EXTRA_START_POSITION, 0) ?: 0
|
||||
autoplay = data?.getBooleanExtra(EXTRA_AUTOPLAY, false) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the previewed file with an external application.
|
||||
*/
|
||||
private fun openFile() {
|
||||
stopPreview(true)
|
||||
fileOperationsHelper.openFile(file)
|
||||
}
|
||||
|
||||
private fun stopPreview(stopAudio: Boolean) {
|
||||
if (MimeTypeUtil.isAudio(file) && stopAudio) {
|
||||
mediaPlayerServiceConnection?.pause()
|
||||
} else if (MimeTypeUtil.isVideo(file)) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
|
||||
exoPlayer?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
val position: Long
|
||||
get() {
|
||||
if (prepared) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
|
||||
}
|
||||
Log_OC.v(TAG, "getting position: $savedPlaybackPosition")
|
||||
return savedPlaybackPosition
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = PreviewMediaActivity::class.java.simpleName
|
||||
const val EXTRA_FILE = "FILE"
|
||||
const val EXTRA_USER = "USER"
|
||||
const val EXTRA_AUTOPLAY = "AUTOPLAY"
|
||||
const val EXTRA_START_POSITION = "START_POSITION"
|
||||
private const val EXTRA_PLAY_POSITION = "PLAY_POSITION"
|
||||
private const val EXTRA_PLAYING = "PLAYING"
|
||||
private const val FILE = "FILE"
|
||||
private const val USER = "USER"
|
||||
private const val PLAYBACK_POSITION = "PLAYBACK_POSITION"
|
||||
private const val AUTOPLAY = "AUTOPLAY"
|
||||
|
||||
/**
|
||||
* Helper method to test if an [OCFile] can be passed to a [PreviewMediaActivity] to be previewed.
|
||||
*
|
||||
* @param file File to test if can be previewed.
|
||||
* @return 'True' if the file can be handled by the activity.
|
||||
*/
|
||||
fun canBePreviewed(file: OCFile?): Boolean {
|
||||
return file != null && (MimeTypeUtil.isAudio(file) || MimeTypeUtil.isVideo(file))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,10 +5,13 @@
|
|||
* @author Chris Narkiewicz
|
||||
* @author Andy Scherzinger
|
||||
* @author TSI-mc
|
||||
* @author Parneet Singh
|
||||
*
|
||||
* Copyright (C) 2016 ownCloud Inc.
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* Copyright (C) 2020 Andy Scherzinger
|
||||
* Copyright (C) 2023 TSI-mc
|
||||
* Copyright (C) 2023 Parneet Singh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2,
|
||||
|
@ -31,7 +34,6 @@ import android.content.res.Configuration;
|
|||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
|
@ -47,12 +49,7 @@ import android.view.MotionEvent;
|
|||
import android.view.View;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
import com.nextcloud.client.di.Injectable;
|
||||
|
@ -88,13 +85,16 @@ import java.util.concurrent.Executors;
|
|||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
|
||||
/**
|
||||
* This fragment shows a preview of a downloaded media file (audio or video).
|
||||
|
@ -106,7 +106,7 @@ import androidx.fragment.app.FragmentManager;
|
|||
* instantiation too.
|
||||
*/
|
||||
public class PreviewMediaFragment extends FileFragment implements OnTouchListener,
|
||||
Injectable, StyledPlayerControlView.OnFullScreenModeChangedListener {
|
||||
Injectable {
|
||||
|
||||
private static final String TAG = PreviewMediaFragment.class.getSimpleName();
|
||||
|
||||
|
@ -374,9 +374,7 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
|
|||
playVideo();
|
||||
});
|
||||
} catch (ClientFactory.CreationException e) {
|
||||
handler.post(() -> {
|
||||
Log_OC.e(TAG, "error setting up ExoPlayer", e);
|
||||
});
|
||||
handler.post(() -> Log_OC.e(TAG, "error setting up ExoPlayer", e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -400,25 +398,12 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
|
|||
activity.toggleActionBarVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void setupVideoView() {
|
||||
binding.exoplayerView.setShowNextButton(false);
|
||||
binding.exoplayerView.setShowPreviousButton(false);
|
||||
binding.exoplayerView.setPlayer(exoPlayer);
|
||||
LinearLayout linearLayout = binding.exoplayerView.findViewById(R.id.exo_center_controls);
|
||||
|
||||
if (linearLayout.getChildCount() == 5) {
|
||||
AppCompatImageButton fullScreenButton = new AppCompatImageButton(requireContext());
|
||||
fullScreenButton.setImageResource(R.drawable.exo_styled_controls_fullscreen_exit);
|
||||
fullScreenButton.setLayoutParams(new LinearLayout.LayoutParams(143, 143));
|
||||
fullScreenButton.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
fullScreenButton.setBackgroundColor(Color.TRANSPARENT);
|
||||
|
||||
fullScreenButton.setOnClickListener(l -> {
|
||||
startFullScreenVideo();
|
||||
});
|
||||
|
||||
linearLayout.addView(fullScreenButton);
|
||||
linearLayout.invalidate();
|
||||
}
|
||||
binding.exoplayerView.setFullscreenButtonClickListener(isFullScreen -> startFullScreenVideo());
|
||||
}
|
||||
|
||||
private void stopAudio() {
|
||||
|
@ -551,11 +536,6 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
|
|||
autoplay = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullScreenModeChanged(boolean isFullScreen) {
|
||||
Log_OC.e(TAG, "Fullscreen: " + isFullScreen);
|
||||
}
|
||||
|
||||
private static class LoadStreamUrl extends AsyncTask<Long, Void, Uri> {
|
||||
|
||||
private final ClientFactory clientFactory;
|
||||
|
|
|
@ -25,19 +25,18 @@ package com.owncloud.android.ui.preview
|
|||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.nextcloud.client.media.ExoplayerListener
|
||||
import com.nextcloud.client.media.NextcloudExoPlayer
|
||||
import com.nextcloud.common.NextcloudClient
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.DialogPreviewVideoBinding
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
|
||||
|
@ -49,15 +48,16 @@ import com.owncloud.android.lib.common.utils.Log_OC
|
|||
* @param sourceExoPlayer the ExoPlayer playing the video
|
||||
* @param sourceView the original non-fullscreen surface that [sourceExoPlayer] is linked to
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
class PreviewVideoFullscreenDialog(
|
||||
private val activity: Activity,
|
||||
nextcloudClient: NextcloudClient,
|
||||
private val sourceExoPlayer: ExoPlayer,
|
||||
private val sourceView: StyledPlayerView
|
||||
private val sourceView: PlayerView
|
||||
) : Dialog(sourceView.context, android.R.style.Theme_Black_NoTitleBar_Fullscreen) {
|
||||
|
||||
private val binding: DialogPreviewVideoBinding = DialogPreviewVideoBinding.inflate(layoutInflater)
|
||||
private var playingStateListener: Player.Listener? = null
|
||||
private var playingStateListener: androidx.media3.common.Player.Listener? = null
|
||||
|
||||
/**
|
||||
* exoPlayer instance used for this view, either the original one or a new one in specific cases.
|
||||
|
@ -112,11 +112,10 @@ class PreviewVideoFullscreenDialog(
|
|||
setOnShowListener {
|
||||
enableImmersiveMode()
|
||||
switchTargetViewFromSource()
|
||||
setListeners()
|
||||
binding.videoPlayer.setFullscreenButtonClickListener { onBackPressed() }
|
||||
if (isPlaying) {
|
||||
mExoPlayer.play()
|
||||
}
|
||||
binding.videoPlayer.showController()
|
||||
}
|
||||
super.show()
|
||||
}
|
||||
|
@ -125,36 +124,10 @@ class PreviewVideoFullscreenDialog(
|
|||
if (shouldUseRotatedVideoWorkaround) {
|
||||
mExoPlayer.seekTo(sourceExoPlayer.currentPosition)
|
||||
} else {
|
||||
StyledPlayerView.switchTargetView(sourceExoPlayer, sourceView, binding.videoPlayer)
|
||||
PlayerView.switchTargetView(sourceExoPlayer, sourceView, binding.videoPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setListeners() {
|
||||
binding.root.findViewById<View>(R.id.exo_exit_fs).setOnClickListener { onBackPressed() }
|
||||
val pauseButton: View = binding.root.findViewById(R.id.exo_pause)
|
||||
pauseButton.setOnClickListener { sourceExoPlayer.pause() }
|
||||
val playButton: View = binding.root.findViewById(R.id.exo_play)
|
||||
playButton.setOnClickListener { sourceExoPlayer.play() }
|
||||
|
||||
val playListener = object : Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
super.onIsPlayingChanged(isPlaying)
|
||||
if (isPlaying) {
|
||||
playButton.visibility = View.GONE
|
||||
pauseButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
playButton.visibility = View.VISIBLE
|
||||
pauseButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
mExoPlayer.addListener(playListener)
|
||||
playingStateListener = playListener
|
||||
|
||||
// Run once to set initial state of play or pause buttons
|
||||
playListener.onIsPlayingChanged(sourceExoPlayer.isPlaying)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val isPlaying = mExoPlayer.isPlaying
|
||||
if (isPlaying) {
|
||||
|
@ -178,7 +151,7 @@ class PreviewVideoFullscreenDialog(
|
|||
if (shouldUseRotatedVideoWorkaround) {
|
||||
sourceExoPlayer.seekTo(mExoPlayer.currentPosition)
|
||||
} else {
|
||||
StyledPlayerView.switchTargetView(sourceExoPlayer, binding.videoPlayer, sourceView)
|
||||
PlayerView.switchTargetView(sourceExoPlayer, binding.videoPlayer, sourceView)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
96
app/src/main/res/layout/activity_preview_media.xml
Normal file
96
app/src/main/res/layout/activity_preview_media.xml
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
ownCloud Android client application
|
||||
|
||||
Copyright (C) 2020 Andy Scherzinger
|
||||
Copyright (C) 2015 ownCloud Inc.
|
||||
Copyright (C) 2023 Parneet Singh
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2,
|
||||
as published by the Free Software Foundation.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-->
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/top"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
tools:context=".ui.preview.PreviewMediaActivity">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/material_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="@dimen/standard_quarter_margin"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:contentDescription="@string/preview_image_description"
|
||||
android:src="@drawable/logo" />
|
||||
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/exoplayer_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
app:show_buffering="always" />
|
||||
|
||||
<com.owncloud.android.media.MediaControlView
|
||||
android:id="@+id/media_controller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.elyeproj.loaderviewlibrary.LoaderImageView
|
||||
android:layout_width="@dimen/empty_list_icon_layout_width"
|
||||
android:layout_height="@dimen/empty_list_icon_layout_width"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@null"
|
||||
app:corners="24" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/empty_list_icon_layout_width"
|
||||
android:layout_height="@dimen/empty_list_icon_layout_height"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@null"
|
||||
android:padding="@dimen/standard_half_padding"
|
||||
android:src="@drawable/file_movie"
|
||||
app:tint="@color/bg_default" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include
|
||||
android:id="@+id/empty_view"
|
||||
layout="@layout/empty_list" />
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -18,12 +18,11 @@
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/videoPlayer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:background="@color/black"
|
||||
app:show_buffering="always"
|
||||
app:controller_layout_id="@layout/exo_player_control_view" />
|
||||
app:show_buffering="always" />
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layoutDirection="ltr"
|
||||
android:background="#CC000000"
|
||||
android:orientation="vertical"
|
||||
tools:targetApi="28">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_prev"
|
||||
style="@style/FullScreenExoControlButton"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/exo_controls_previous"
|
||||
android:contentDescription="@string/exo_controls_previous_description" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_rew"
|
||||
style="@style/FullScreenExoControlButton"
|
||||
android:contentDescription="@string/exo_controls_rewind_description"
|
||||
android:src="@drawable/exo_controls_rewind" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_play"
|
||||
style="@style/FullScreenExoControlButton"
|
||||
android:contentDescription="@string/exo_controls_play_description"
|
||||
android:src="@drawable/exo_controls_play"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_pause"
|
||||
style="@style/FullScreenExoControlButton"
|
||||
android:contentDescription="@string/exo_controls_pause_description"
|
||||
android:src="@drawable/exo_controls_pause" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_ffwd"
|
||||
style="@style/FullScreenExoControlButton"
|
||||
android:contentDescription="@string/exo_controls_fastforward_description"
|
||||
android:src="@drawable/exo_controls_fastforward" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exo_exit_fs"
|
||||
style="@style/FullScreenExoControlButton"
|
||||
android:contentDescription="@string/exo_controls_fullscreen_exit_description"
|
||||
android:src="@drawable/exo_styled_controls_fullscreen_exit" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginStart="@dimen/standard_margin"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="#FFBEBEBE" />
|
||||
|
||||
<View
|
||||
android:id="@id/exo_progress_placeholder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="26dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="#FFBEBEBE" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -21,14 +21,15 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include android:id="@+id/appbar" layout="@layout/toolbar_standard" />
|
||||
<include
|
||||
android:id="@+id/appbar"
|
||||
layout="@layout/toolbar_standard" />
|
||||
|
||||
<!-- The main content view -->
|
||||
<LinearLayout
|
||||
|
@ -62,7 +63,7 @@
|
|||
android:visibility="gone"
|
||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||
app:srcCompat="@drawable/ic_plus"
|
||||
tools:visibility="visible"/>
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
ownCloud Android client application
|
||||
|
||||
Copyright (C) 2020 Andy Scherzinger
|
||||
Copyright (C) 2015 ownCloud Inc.
|
||||
Copyright (C) 2023 Parneet Singh
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2,
|
||||
|
@ -20,8 +20,8 @@
|
|||
-->
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/top"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -38,13 +38,12 @@
|
|||
android:src="@drawable/logo" />
|
||||
|
||||
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/exoplayer_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
app:show_buffering="always"
|
||||
app:show_next_button="false" />
|
||||
app:show_buffering="always" />
|
||||
|
||||
<com.owncloud.android.media.MediaControlView
|
||||
android:id="@+id/media_controller"
|
||||
|
@ -69,9 +68,9 @@
|
|||
<ImageView
|
||||
android:layout_width="@dimen/empty_list_icon_layout_width"
|
||||
android:layout_height="@dimen/empty_list_icon_layout_height"
|
||||
android:padding="@dimen/standard_half_padding"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@null"
|
||||
android:padding="@dimen/standard_half_padding"
|
||||
android:src="@drawable/file_movie"
|
||||
app:tint="@color/bg_default" />
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Nextcloud Android client application
|
||||
|
||||
Copyright (C) 2020 Nextcloud
|
||||
|
@ -48,4 +47,8 @@
|
|||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.ownCloud.Media" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -454,10 +454,6 @@
|
|||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="FullScreenExoControlButton" parent="ExoStyledControls.Button.Center">
|
||||
<item name="android:background">@drawable/ripple</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Nextcloud.AppWidget.Container" parent="android:Widget">
|
||||
<item name="android:id">@android:id/background</item>
|
||||
<item name="android:background">?android:attr/colorBackground</item>
|
||||
|
|
Loading…
Reference in a new issue