Compare commits

...

1 Commits

Author SHA1 Message Date
eggmanQQQ
927059e873 try remove liulishuo download lib 2024-10-15 18:27:27 +08:00
26 changed files with 307 additions and 962 deletions

View File

@@ -13,7 +13,7 @@
android:protectionLevel="signature" /> <!-- 谷歌内购权限 -->
<permission
android:name="${applicationId}.push.permission.MESSAGE"
android:protectionLevel="signature" /> <!-- Required -->
android:protectionLevel="signature" /> <!-- Required -->
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />

View File

@@ -306,7 +306,7 @@ public class MainActivity extends BaseMvpActivity<IMainView, MainPresenter>
PreloadResourceViewModel viewModel = new ViewModelProvider(
GlobalViewModelOwner.Companion.getInstance()
).get(PreloadResourceViewModel.class);
viewModel.start();
viewModel.start(context);
}
@Override

View File

@@ -25,7 +25,6 @@ import com.chwl.library.language.LanguageHelper;
import com.coorchice.library.utils.LogUtils;
import com.example.lib_utils.ServiceTime;
import com.hjq.toast.ToastUtils;
import com.liulishuo.filedownloader.FileDownloader;
import com.netease.nim.uikit.api.NimUIKit;
import com.netease.nim.uikit.common.util.log.LogUtil;
import com.netease.nimlib.sdk.NIMClient;
@@ -459,7 +458,6 @@ public class App extends BaseApp {
.build();
Realm.setDefaultConfiguration(config);
FileDownloader.setup(BasicConfig.INSTANCE.getAppContext());
LogUtil.i(TAG, channel);
}

View File

@@ -12,13 +12,13 @@ import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.chwl.library.utils.PathHelper;
import com.chwl.library.download.DownloadException;
import com.chwl.library.download.DownloadManager;
import com.chwl.library.download.DownloadRequest;
import com.chwl.library.download.DownloadTask;
import com.chwl.library.download.FileDownloadListener;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.chwl.library.common.glide.GlideUtils;
import com.example.lib_utils.log.LogUtil;
import com.netease.nim.uikit.common.util.string.StringUtil;
import com.opensource.svgaplayer.SVGACallback;
@@ -205,22 +205,44 @@ public class GiftEffectView extends RelativeLayout {
private void drawSvgaEffect(String url) {
log("drawSvgaEffect url:" + url);
String filePath = PathHelper.INSTANCE.generateResourcesFilePath(url);
DownloadRequest request = DownloadRequest.Companion.build(url, filePath, DOWNLOAD_TAG, null, 60000L);
DownloadManager.INSTANCE.download(request, new FileDownloadListener() {
GlideUtils.instance().downloadFromUrl(getContext(), url, new RequestListener<File>() {
@Override
public void onDownloadCompleted(@NonNull DownloadTask task) {
String path = task.getRequest().getPath();
log("drawSvgaEffect onDownloadCompleted url:" + url + " path:" + path);
drawSvgaEffectFile(path);
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
log("drawSvgaEffect onDownloadError url:" + url);
effectHandler.sendEmptyMessage(0);
return true;
}
@Override
public void onDownloadError(@NonNull DownloadException exception) {
log("drawSvgaEffect onDownloadError url:" + url);
effectHandler.sendEmptyMessage(0);
public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
if (resource != null) {
String path = resource.getPath();
log("drawSvgaEffect onDownloadCompleted url:" + url + " path:" + path);
drawSvgaEffectFile(path);
}
return true;
}
});
// String filePath = PathHelper.INSTANCE.generateResourcesFilePath(url);
// DownloadRequest request = DownloadRequest.Companion.build(url, filePath, DOWNLOAD_TAG, null, 60000L);
// DownloadManager.INSTANCE.download(request, new FileDownloadListener() {
// @Override
// public void onDownloadCompleted(@NonNull DownloadTask task) {
// String path = task.getRequest().getPath();
// log("drawSvgaEffect onDownloadCompleted url:" + url + " path:" + path);
// drawSvgaEffectFile(path);
// }
//
// @Override
// public void onDownloadError(@NonNull DownloadException exception) {
// log("drawSvgaEffect onDownloadError url:" + url);
// effectHandler.sendEmptyMessage(0);
// }
// });
}
private void drawSvgaEffectFile(String path) {
@@ -236,23 +258,45 @@ public class GiftEffectView extends RelativeLayout {
private void drawVAPEffect(String url) {
log("drawVAPEffect url:" + url);
String filePath = PathHelper.INSTANCE.generateResourcesFilePath(url);
DownloadRequest request = DownloadRequest.Companion.build(url, filePath, DOWNLOAD_TAG, null, 60000L);
DownloadManager.INSTANCE.download(request, new FileDownloadListener() {
GlideUtils.instance().downloadFromUrl(getContext(), url, new RequestListener<File>() {
@Override
public void onDownloadCompleted(@NonNull DownloadTask task) {
String path = task.getRequest().getPath();
log("drawVAPEffect onDownloadCompleted url:" + url + " path:" + path);
vapAnimView.startPlay(new File(path));
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
log("drawVAPEffect onDownloadError url:" + url);
effectHandler.sendEmptyMessageDelayed(0, 4000);
return true;
}
@Override
public void onDownloadError(@NonNull DownloadException exception) {
log("drawVAPEffect onDownloadError url:" + url);
exception.printStackTrace();
effectHandler.sendEmptyMessageDelayed(0, 4000);
public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
if (resource != null) {
log("drawVAPEffect onDownloadCompleted url:" + url + " path:" + resource.getPath());
vapAnimView.startPlay(resource);
}
return true;
}
});
// String filePath = PathHelper.INSTANCE.generateResourcesFilePath(url);
// DownloadRequest request = DownloadRequest.Companion.build(url, filePath, DOWNLOAD_TAG, null, 60000L);
// DownloadManager.INSTANCE.download(request, new FileDownloadListener() {
// @Override
// public void onDownloadCompleted(@NonNull DownloadTask task) {
// String path = task.getRequest().getPath();
// log("drawVAPEffect onDownloadCompleted url:" + url + " path:" + path);
// vapAnimView.startPlay(new File(path));
// }
//
// @Override
// public void onDownloadError(@NonNull DownloadException exception) {
// log("drawVAPEffect onDownloadError url:" + url);
// exception.printStackTrace();
// effectHandler.sendEmptyMessageDelayed(0, 4000);
// }
// });
}
private void deleteAnim() {
@@ -270,7 +314,6 @@ public class GiftEffectView extends RelativeLayout {
public void release() {
log("release");
DownloadManager.INSTANCE.stopTag(DOWNLOAD_TAG);
effectHandler.removeMessages(0);
}

View File

@@ -3,7 +3,12 @@ package com.chwl.app.decoration.adapter;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.chwl.app.R;
import com.chwl.app.bindadapter.BaseAdapter;
import com.chwl.app.bindadapter.BindingViewHolder;
@@ -11,13 +16,8 @@ import com.chwl.app.databinding.ItemMyUserCardWearBinding;
import com.chwl.app.ui.utils.ImageLoadUtils;
import com.chwl.core.decoration.headwear.bean.HeadWearInfo;
import com.chwl.core.decoration.headwear.bean.UserCardWearInfo;
import com.chwl.library.download.DownloadException;
import com.chwl.library.download.DownloadManager;
import com.chwl.library.download.DownloadRequest;
import com.chwl.library.download.DownloadTask;
import com.chwl.library.download.FileDownloadListener;
import com.chwl.library.common.glide.GlideUtils;
import com.chwl.library.utils.ResUtil;
import com.chwl.library.utils.PathHelper;
import java.io.File;
@@ -37,21 +37,37 @@ public class MyUserCardWearAdapter extends BaseAdapter<UserCardWearInfo> {
ItemMyUserCardWearBinding binding = (ItemMyUserCardWearBinding) helper.getBinding();
binding.ivUserCardWearMp4.setLoop(Integer.MAX_VALUE);
String filePath = PathHelper.INSTANCE.generateResourcesFilePath(item.getPic());
DownloadRequest request = DownloadRequest.Companion.build(item.getPic(), filePath, "gift_effect_download", null, 60000L);
DownloadManager.INSTANCE.download(request, new FileDownloadListener() {
GlideUtils.instance().downloadFromUrl(binding.ivUserCardWearMp4.getContext(), item.getPic(), new RequestListener<File>() {
@Override
public void onDownloadCompleted(@NonNull DownloadTask task) {
String path = task.getRequest().getPath();
binding.ivUserCardWearMp4.startPlay(new File(path));
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
ImageLoadUtils.loadImage(mContext, item.getPic(), binding.ivUserCardWear);
return true;
}
@Override
public void onDownloadError(@NonNull DownloadException exception) {
ImageLoadUtils.loadImage(mContext, item.getPic(), binding.ivUserCardWear);
public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
if (resource != null) {
binding.ivUserCardWearMp4.startPlay(resource);
}
return true;
}
});
// String filePath = PathHelper.INSTANCE.generateResourcesFilePath(item.getPic());
// DownloadRequest request = DownloadRequest.Companion.build(item.getPic(), filePath, "gift_effect_download", null, 60000L);
// DownloadManager.INSTANCE.download(request, new FileDownloadListener() {
// @Override
// public void onDownloadCompleted(@NonNull DownloadTask task) {
// String path = task.getRequest().getPath();
// binding.ivUserCardWearMp4.startPlay(new File(path));
// }
//
// @Override
// public void onDownloadError(@NonNull DownloadException exception) {
// ImageLoadUtils.loadImage(mContext, item.getPic(), binding.ivUserCardWear);
// }
// });
helper.addOnClickListener(R.id.tv_used);

View File

@@ -36,9 +36,11 @@ import com.chwl.app.ui.relation.FansListActivity
import com.chwl.app.ui.utils.ImageLoadUtils
import com.chwl.app.ui.wallet.WalletActivity
import com.chwl.app.ui.webview.CommonWebViewActivity
import com.chwl.app.ui.widget.UserInfoDialog
import com.chwl.app.view.GenderAgeTextView
import com.chwl.app.vip.VipCenterActivity
import com.chwl.app.vip.VipViewModel
import com.chwl.app.vip.dialog.VipUpgradeDialog
import com.chwl.core.auth.AuthModel
import com.chwl.core.gift.bean.BoomMsgAwardBean
import com.chwl.core.initial.InitialModel
@@ -373,16 +375,21 @@ class MeFragment : BaseFragment(), View.OnClickListener {
//todo do 测试按钮
if (BuildConfig.DEBUG) {
val data1 = arrayListOf<BoomMsgAwardBean>()
for (i in 0 until 10) {
data1.add(BoomMsgAwardBean().apply {
awardPic = "http://beta.img.pekolive.com/FpHRJteaNhJrb-ZBvS3cweeZoPV4?imageslim"
uid = AuthModel.get().currentUid
})
}
RoomBoomRewardDialog(requireContext()).apply {
list = data1
}.show()
// val data1 = arrayListOf<BoomMsgAwardBean>()
// for (i in 0 until 10) {
// data1.add(BoomMsgAwardBean().apply {
// awardPic = "http://beta.img.pekolive.com/FpHRJteaNhJrb-ZBvS3cweeZoPV4?imageslim"
// uid = AuthModel.get().currentUid
// })
// }
// RoomBoomRewardDialog(requireContext()).apply {
// list = data1
// }.show()
// VipUpgradeDialog.newInstance(VipInfo(vipName = "v1", vipLogo = "https://image.pekolive.com/v5.mp4")).show(requireContext())
UserInfoDialog.showNewUserInfoDialog(requireContext(),AuthModel.get().currentUid)
}
}

View File

@@ -1,16 +1,18 @@
package com.chwl.app.support
import android.annotation.SuppressLint
import android.content.Context
import androidx.lifecycle.viewModelScope
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.chwl.app.BuildConfig
import com.chwl.app.base.BaseViewModel
import com.chwl.library.utils.PathHelper
import com.chwl.core.home.model.HomeModel
import com.chwl.library.common.glide.GlideUtils
import com.chwl.library.common.util.SPUtils
import com.chwl.library.download.DownloadException
import com.chwl.library.download.DownloadListener
import com.chwl.library.download.DownloadManager
import com.chwl.library.download.DownloadRequest
import com.chwl.library.download.DownloadTask
import com.chwl.library.utils.NetworkUtils
import com.example.lib_utils.AppUtils
import com.example.lib_utils.FileUtils2
@@ -18,8 +20,10 @@ import com.example.lib_utils.log.ILog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
class PreloadResourceViewModel : BaseViewModel(), DownloadListener, ILog {
@SuppressLint("StaticFieldLeak")
class PreloadResourceViewModel : BaseViewModel(), ILog , RequestListener<File?> {
private val DOWNLOAD_TAG = "RESOURCE_DOWNLOAD_TAG"
@@ -27,7 +31,10 @@ class PreloadResourceViewModel : BaseViewModel(), DownloadListener, ILog {
private var isStarted = false
fun start() {
private var mContext : Context? = null
fun start(context: Context) {
mContext = context
if (BuildConfig.DEBUG) {
// 太多请求了,影响查看控制台日志
return
@@ -85,40 +92,53 @@ class PreloadResourceViewModel : BaseViewModel(), DownloadListener, ILog {
}
private fun downloadTask(url: String) {
val path = PathHelper.generateResourcesFilePath(url)
logI("downloadTask() url:${url} path:${path}")
val request = DownloadRequest.build(
url = url,
path = path,
tag = DOWNLOAD_TAG,
header = null,
timeout = null
)
DownloadManager.download(request, this)
// val path = PathHelper.generateResourcesFilePath(url)
// logI("downloadTask() url:${url} path:${path}")
// val request = DownloadRequest.build(
// url = url,
// path = path,
// tag = DOWNLOAD_TAG,
// header = null,
// timeout = null
// )
// DownloadManager.download(request, this)
GlideUtils.instance().downloadFromUrl(mContext,url,this)
}
override fun onDownloadError(exception: DownloadException) {
super.onDownloadError(exception)
val url = exception.task()?.getRequest()?.getUrl()
logI(
"onDownloadError() url:${url} message:${exception.message}"
)
exception.printStackTrace()
nextTask(url)
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File?>?,
isFirstResource: Boolean
): Boolean {
if (model is String) {
logI("onDownloadError() url:${model} message:${e?.message}")
nextTask(model)
}
return true
}
override fun onDownloadCompleted(task: DownloadTask) {
super.onDownloadCompleted(task)
val url = task.getRequest().getUrl()
logI(
"onDownloadCompleted() url:${url} path:${
task.getRequest().getPath()
}"
)
makeDownloadRecord(url)
nextTask(url)
override fun onResourceReady(
resource: File?,
model: Any?,
target: Target<File?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
if (model is String) {
logI("onDownloadCompleted() url:${model} path:${resource?.path}")
makeDownloadRecord(model)
nextTask(null)
}
return true
}
private fun isWifiNet(): Boolean {
return getNetType() == NetworkUtils.NET_WIFI
}
@@ -149,10 +169,12 @@ class PreloadResourceViewModel : BaseViewModel(), DownloadListener, ILog {
val set = getDownloadRecord()
set.add(url)
SPUtils.putStringSet("RESOURCE_DOWNLOAD_COMPLETE", set)
preloadResourceList?.remove(url)
}
override fun onCleared() {
super.onCleared()
DownloadManager.stopTag(DOWNLOAD_TAG)
}
}

View File

@@ -5,12 +5,8 @@ import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.BaseViewHolder
import com.chwl.app.R
import com.chwl.app.ui.utils.load
import com.chwl.app.ui.utils.loadAnim2
import com.chwl.core.user.bean.MedalBean
import com.chwl.library.download.DownloadManager
import com.chwl.library.download.DownloadRequest
import com.chwl.library.download.DownloadTask
import com.chwl.library.download.FileDownloadListener
import com.chwl.library.utils.PathHelper
import com.tencent.qgame.animplayer.AnimConfig
import com.tencent.qgame.animplayer.AnimView
import com.tencent.qgame.animplayer.inter.IAnimListener
@@ -46,14 +42,7 @@ class UserInfoMedalAdapter :
}
})
val filePath = PathHelper.generateResourcesFilePath(it)
val request = DownloadRequest.build(it, filePath, "", null, 600000L)
DownloadManager.download(request, object : FileDownloadListener() {
override fun onDownloadCompleted(task: DownloadTask) {
val path = task.getRequest().getPath()
mp4View.startPlay(File(path))
}
})
mp4View.loadAnim2(it)
} else {
val imageView = helper.getView<ImageView>(R.id.iv_image)
imageView.load(item.picUrl,0f,R.drawable.transparent_draw)

View File

@@ -201,7 +201,9 @@ fun AnimView.loadAnim2(url: String) {
isFirstResource: Boolean
): Boolean {
if (resource != null) {
this@loadAnim2.startPlay(resource)
if (this@loadAnim2.isAttachedToWindow){
this@loadAnim2.startPlay(resource)
}
}
return true
}

View File

@@ -32,14 +32,13 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.chwl.app.utils.AvatarHelper;
import com.chwl.core.vip.bean.UserVipInfo;
import com.chwl.library.download.DownloadException;
import com.chwl.library.download.DownloadManager;
import com.chwl.library.download.DownloadRequest;
import com.chwl.library.download.DownloadTask;
import com.chwl.library.download.FileDownloadListener;
import com.chwl.library.utils.PathHelper;
import com.chwl.library.common.glide.GlideUtils;
import com.chwl.library.widget.SVGAView;
import com.google.android.flexbox.FlexboxLayout;
import com.netease.nim.uikit.common.util.sys.ScreenUtil;
@@ -659,20 +658,38 @@ public class UserInfoDialog extends AppCompatDialog implements View.OnClickListe
}
private void drawVAPEffect(String url) {
String filePath = PathHelper.INSTANCE.generateResourcesFilePath(url);
DownloadRequest request = DownloadRequest.Companion.build(url, filePath, "gift_effect_download", null, 60000L);
DownloadManager.INSTANCE.download(request, new FileDownloadListener() {
GlideUtils.instance().downloadFromUrl(getContext(), url, new RequestListener<File>() {
@Override
public void onDownloadCompleted(@NonNull DownloadTask task) {
String path = task.getRequest().getPath();
ivUserCardWearMP4.startPlay(new File(path));
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
ImageLoadUtils.loadImage(context, userInfo.getUserInfoCardPic(), ivUserCardWear);
return true;
}
@Override
public void onDownloadError(@NonNull DownloadException exception) {
ImageLoadUtils.loadImage(context, userInfo.getUserInfoCardPic(), ivUserCardWear);
public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
if (resource != null) {
ivUserCardWearMP4.startPlay(resource);
}
return true;
}
});
// String filePath = PathHelper.INSTANCE.generateResourcesFilePath(url);
// DownloadRequest request = DownloadRequest.Companion.build(url, filePath, "gift_effect_download", null, 60000L);
// DownloadManager.INSTANCE.download(request, new FileDownloadListener() {
// @Override
// public void onDownloadCompleted(@NonNull DownloadTask task) {
// String path = task.getRequest().getPath();
// ivUserCardWearMP4.startPlay(new File(path));
// }
//
// @Override
// public void onDownloadError(@NonNull DownloadException exception) {
// ImageLoadUtils.loadImage(context, userInfo.getUserInfoCardPic(), ivUserCardWear);
// }
// });
}
private void copyName() {

View File

@@ -13,12 +13,6 @@ import com.bumptech.glide.request.target.Target
import com.chwl.app.R
import com.chwl.core.vip.bean.VipInfo
import com.chwl.library.common.glide.GlideUtils
import com.chwl.library.download.DownloadException
import com.chwl.library.download.DownloadManager.download
import com.chwl.library.download.DownloadRequest.Companion.build
import com.chwl.library.download.DownloadTask
import com.chwl.library.download.FileDownloadListener
import com.chwl.library.utils.PathHelper.generateResourcesFilePath
import com.chwl.library.utils.ResUtil.getString
import com.tencent.qgame.animplayer.AnimConfig
import com.tencent.qgame.animplayer.AnimView

View File

@@ -10,18 +10,10 @@ import com.opensource.svgaplayer.SVGAVideoEntity
import com.chwl.app.base.BaseActivity
import com.chwl.app.base.BaseDialogFragment
import com.chwl.app.databinding.DialogVipUpgradeBinding
import com.chwl.app.ui.utils.loadAnim
import com.chwl.app.ui.utils.loadAnim2
import com.chwl.app.vip.VipViewModel
import com.chwl.core.vip.bean.VipInfo
import com.chwl.library.download.DownloadException
import com.chwl.library.download.DownloadManager.download
import com.chwl.library.download.DownloadRequest
import com.chwl.library.download.DownloadRequest.Companion
import com.chwl.library.download.DownloadTask
import com.chwl.library.download.FileDownloadListener
import com.chwl.library.utils.PathHelper.generateResourcesFilePath
import java.io.File
import java.net.MalformedURLException
import java.net.URL
class VipUpgradeDialog : BaseDialogFragment<DialogVipUpgradeBinding>() {
@@ -62,19 +54,9 @@ class VipUpgradeDialog : BaseDialogFragment<DialogVipUpgradeBinding>() {
binding?.tvVipUplevel?.setOnClickListener { dismissAllowingStateLoss() }
binding?.tvVipOrigin?.setOnClickListener { vm.saveOriginDisguise() }
binding?.ivVipIconMp4?.setLoop(Int.MAX_VALUE)
binding?.ivVipIconMp4?.loadAnim(vipInfo.vipLogo)
val filePath = generateResourcesFilePath(vipInfo.vipLogo)
val request = DownloadRequest.build(vipInfo.vipLogo, filePath, "gift_effect_download", null, 60000L)
download(request, object : FileDownloadListener() {
override fun onDownloadCompleted(task: DownloadTask) {
val path = task.getRequest().getPath()
binding?.ivVipIconMp4?.startPlay(File(path))
}
override fun onDownloadError(exception: DownloadException) {
exception.printStackTrace()
}
})
// try {
// SVGAParser.shareParser()

View File

@@ -71,11 +71,11 @@ dependencies {
api 'io.agora.rtc:voice-sdk:4.2.2'
// core
implementation 'com.liulishuo.okdownload:okdownload:1.0.4'
// implementation 'com.liulishuo.okdownload:okdownload:1.0.4'
// provide sqlite to store breakpoints
implementation 'com.liulishuo.okdownload:sqlite:1.0.4'
// implementation 'com.liulishuo.okdownload:sqlite:1.0.4'
// provide okhttp to connect to backend
implementation 'com.liulishuo.okdownload:okhttp:1.0.4'
// implementation 'com.liulishuo.okdownload:okhttp:1.0.4'
// Room
api 'androidx.room:room-runtime:2.5.1'
annotationProcessor 'androidx.room:room-compiler:2.5.1'
@@ -93,8 +93,8 @@ dependencies {
api project(':nim_uikit')
api project(':library')
implementation 'com.liulishuo.okdownload:okdownload:1.0.7'
implementation 'com.liulishuo.okdownload:okhttp:1.0.7'
// implementation 'com.liulishuo.okdownload:okdownload:1.0.7'
// implementation 'com.liulishuo.okdownload:okhttp:1.0.7'
implementation 'com.tencent.liteav:LiteAVSDK_TRTC:11.7.0.13946'

View File

@@ -10,10 +10,13 @@ import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.chwl.core.utils.MyUriUtils;
import com.chwl.library.common.glide.GlideUtils;
import com.google.gson.Gson;
import com.liulishuo.okdownload.DownloadTask;
import com.liulishuo.okdownload.core.cause.EndCause;
import com.liulishuo.okdownload.core.listener.DownloadListener2;
import com.netease.nim.uikit.common.util.string.StringUtil;
import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder;
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
@@ -52,6 +55,7 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@@ -63,6 +67,11 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.BiConsumer;
import io.reactivex.functions.Consumer;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import retrofit2.http.GET;
import retrofit2.http.Headers;
@@ -621,27 +630,47 @@ public class DynamicFaceModel extends BaseModel implements IDynamicFaceModel {
}
}
String url = onlineFacesList.getZipUrl();
DownloadTask task = new DownloadTask.Builder(url, facesRootDir)
.setFilename("face.zip")
.setMinIntervalMillisCallbackProcess(1000)
.setPassIfAlreadyCompleted(true)
.build();
task.enqueue(new DownloadListener2() {
GlideUtils.instance().downloadFromUrl(getContext(), url, new RequestListener<File>() {
@Override
public void taskStart(@NonNull DownloadTask task) {
Logger.d("face.zip download start");
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
Logger.d("face.zip download onLoadFailed");
return true;
}
@Override
public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause) {
Logger.d("face.zip download end" + "response dir: " + facesRootDir.getAbsolutePath());
unzipFaceZipFile(onlineFacesList);
isRequestingZip = false;
public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
if (resource != null) {
MyUriUtils.INSTANCE.copyFile(resource,new File(facesRootDir,"face.zip"));
Logger.d("face.zip download end" + "response dir: " + facesRootDir.getAbsolutePath());
unzipFaceZipFile(onlineFacesList);
isRequestingZip = false;
}
return true;
}
});
// DownloadTask task = new DownloadTask.Builder(url, facesRootDir)
// .setFilename("face.zip")
// .setMinIntervalMillisCallbackProcess(1000)
// .setPassIfAlreadyCompleted(true)
// .build();
//
// task.enqueue(new DownloadListener2() {
// @Override
// public void taskStart(@NonNull DownloadTask task) {
// Logger.d("face.zip download start");
// }
//
// @Override
// public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause) {
// Logger.d("face.zip download end" + "response dir: " + facesRootDir.getAbsolutePath());
// unzipFaceZipFile(onlineFacesList);
// isRequestingZip = false;
// }
// });
}
/**

View File

@@ -70,5 +70,12 @@ object MyUriUtils {
return data
}
fun copyFile(file:File,dir:File){
if (!dir.exists()) {
dir.mkdirs()
}
file.copyTo(dir,true)
}
}

View File

@@ -119,7 +119,7 @@ dependencies {
api project(':libs:lib_encipher')
api 'com.qcloud.cos:cos-android:5.9.25'
api 'com.liulishuo.filedownloader:library:1.7.7'
// api 'com.liulishuo.filedownloader:library:1.7.7'
api "com.github.yyued:SVGAPlayer-Android:2.6.1"
}

View File

@@ -1,26 +0,0 @@
package com.chwl.library.download
class DownloadException : Exception {
private var task: DownloadTask? = null
private var request: DownloadRequest? = null
constructor(message: String) : super(message)
constructor(task: DownloadTask, throwable: Throwable) : super(
throwable
) {
this.task = task
this.request = task.getRequest()
}
constructor(request: DownloadRequest, throwable: Throwable) : super(
throwable
) {
this.request = request
}
fun task() = task
}

View File

@@ -1,22 +0,0 @@
package com.chwl.library.download
interface DownloadListener {
/**
* 下载完成
*/
fun onDownloadCompleted(task: DownloadTask) {}
/**
* 下载进度
* @param soFarBytes 已下载
* @param totalBytes 总
*/
fun onDownloadProgress(task: DownloadTask, soFarBytes: Int, totalBytes: Int) {}
/**
* 下载异常
*/
fun onDownloadError(exception: DownloadException){}
}

View File

@@ -1,231 +0,0 @@
package com.chwl.library.download
import com.example.lib_utils.log.ILog
import java.util.concurrent.ConcurrentHashMap
object DownloadManager : ILog {
/**
* 任务列表 <URL,任务>
*/
private val tasks: ConcurrentHashMap<String, DownloadTask> by lazy {
ConcurrentHashMap()
}
/**
* 标签列表 <TAG,URL>
*/
private val tags: ConcurrentHashMap<String, HashSet<String>> by lazy {
ConcurrentHashMap()
}
/**
* 停止所有任务
*/
fun stopAll() {
logD("stopAll()")
tasks.keys.map {
it
}.forEach {
stop(it)
}
tasks.clear()
tags.clear()
}
/**
* 停止某一个URL下载任务有多组下载该URL都会被停止
*/
fun stop(url: String) {
logD("stop() url:${url}")
getTask(url)?.stop()
}
/**
* 停止某一组内的某个URL下载任务其他组正在下载该URL的任务不受影响
*/
fun stop(url: String, tag: String) {
logD("stop() url:$url ,tag:$tag")
getTask(url)?.removeListener(tag)
}
/**
* 停止某一标签的所有任务
*/
fun stopTag(tag: String) {
logD("stopTag()")
if (tags.containsKey(tag)) {
tags.remove(tag)?.let {
it.forEach { url ->
getTask(url)?.removeListener(tag)
}
}
}
}
/**
* 停止某个下载请求
*/
fun stop(request: DownloadRequest) {
stop(request.getUrl(), request.getTag())
}
/**
* 下载文件 (简单下载,也可通过自己构造DownloadRequest下载)
* @param url 下载地址
* @param tag 标签
* @param listener 监听器
*/
fun download(url: String, path: String, listener: DownloadListener, tag: String? = null) {
val request = DownloadRequest.build(url, path, tag, null)
download(request, listener)
}
/**
* 下载文件
* @param request 下载请求体
* @param listener 监听器
*/
fun download(request: DownloadRequest, listener: DownloadListener) {
download(request, request.getTag(), listener)
}
/**
* 下载文件
* @param request 下载请求体
* @param listenerTag 监听器标签(有重复任务时,内部会复用任务追加监听器,监听器以标签区分。)
* @param listener 监听器
*/
fun download(request: DownloadRequest, listenerTag: String, listener: DownloadListener) {
logD("download() url:${request.getUrl()} ,path:${request.getPath()} ,listenerTag:$listenerTag")
// 记录标签与地址关系
addTag(request.getTag(), request.getUrl())
synchronized(this) {
var task = getTask(request)
// 判断是否有相同目标的任务
if (task != null) {
logD("download() 有重复任务->关联复用")
task.addListener(listenerTag, listener)
task.start()
} else {
logD("download() 创建新任务")
task = request.createTask().apply {
addListener(listenerTag, listener)
}
addTask(task)
task.start()
}
}
}
/**
* 添加监听
* @param url 下载地址
* @param listener 监听器
*/
fun addListener(url: String, listener: DownloadListener) {
getTask(url)?.addListener(listener = listener)
}
/**
* 移除监听
* @param url 下载地址
* @param listener 监听器
*/
fun removeListener(url: String, listener: DownloadListener) {
getTask(url)?.removeListener(listener = listener)
}
/**
* 获取任务(具有相同目标的任务)
* @param request
*/
fun getTask(request: DownloadRequest): DownloadTask? {
tasks.values.forEach {
if (it.getRequest().equalsTarget(request)) {
return it
}
}
return null
}
/**
* 添加任务
*/
private fun addTask(task: DownloadTask) {
logD("addTask()")
tasks[task.getRequest().getUrl()] = task
}
/**
* 移除任务
*/
fun removeTask(task: DownloadTask) {
removeTask(task.getRequest().getUrl())
}
/**
* 移除任务
*/
@Synchronized
private fun removeTask(url: String) {
logD("removeTask() url:$url")
tasks.remove(url)?.apply {
// 移除该URL与标签关系
tags.iterator().let {
while (it.hasNext()) {
it.next().let { entry ->
entry.value.remove(url)
if (entry.value.isEmpty()) {
it.remove()
}
}
}
}
}
}
/**
* 获取任务-根据下载地址
* @param url 下载地址
*/
fun getTask(url: String): DownloadTask? {
if (hasTask(url)) {
return tasks[url]
}
return null
}
/**
* 是否存在相同任务
* @param url 下载地址
*/
private fun hasTask(url: String): Boolean {
return tasks.containsKey(url)
}
/**
* 是否存在相同任务
*/
private fun hasTask(request: DownloadRequest): Boolean {
return getTask(request) != null
}
/**
* 添加标签记录(建立关系)
*/
private fun addTag(tag: String, url: String) {
logD("addTag() tag:$tag ,url:$url")
if (tag.isEmpty() || url.isEmpty()) {
return
}
synchronized(this) {
tags[tag]?.add(url) ?: let {
tags[tag] = HashSet<String>().apply {
add(url)
}
}
}
}
}

View File

@@ -1,60 +0,0 @@
package com.chwl.library.download
interface DownloadRequest {
companion object {
/**
* 构建请求
* @param url 下载地址
* @param path 存储路径(完整)
* @param tag 标记
* @param header 请求头
* @param timeout 下载超时时间
*/
fun build(
url: String,
path: String,
tag: String? = null,
header: Map<String, String>? = null,
timeout: Long? = null,
): DownloadRequest {
return FileDownloadRequest(url, path, tag, header, timeout)
}
}
/**
* 下载地址
*/
fun getUrl(): String
/**
* 本地文件路径
*/
fun getPath(): String
/**
* 标签
*/
fun getTag(): String
/**
* 请求头
*/
fun getHeader(): Map<String, String>?
/**
* 获取下载任务
*/
fun createTask(): DownloadTask
/**
* 获取下载超时时间(毫秒)
*/
fun getTimeout(): Long?
/**
* 是否是相同的目标
*/
fun equalsTarget(request: DownloadRequest): Boolean
}

View File

@@ -1,36 +0,0 @@
package com.chwl.library.download
object DownloadState {
/**
* 空闲
*/
const val IDLE: Int = -1
/**
* 错误
*/
const val ERROR: Int = 0
/**
* 成功
*/
const val SUCCESS: Int = 1
/**
* 已启动
*/
const val STARTED: Int = 2
/**
* 进行中
*/
const val PROGRESS: Int = 3
/**
* 暂停
*/
const val PAUSED: Int = 4
}

View File

@@ -1,64 +0,0 @@
package com.chwl.library.download
import com.example.lib_utils.ICleared
interface DownloadTask : ICleared {
/**
* 请求体
*/
fun getRequest(): DownloadRequest
/**
* 获取当前状态
*/
fun getState(): Int
/**
* 任务ID(唯一标识)
*/
fun getId(): String
/**
* 文件路径(只有成功时才能拿到有效的文件路径)
*/
fun getFilePath(): String?
/**
* 开始/恢复
*/
fun start()
/**
* 停止/暂停
*/
fun stop()
/**
* 是否暂停状态
*/
fun isPause(): Boolean {
return getState() == DownloadState.PAUSED
}
/**
* 添加回调监听
* @param tag 标签 (重复标签时,监听器会发生覆盖操作)
* @param listener 监听器
* @return 是否添加成功
*/
fun addListener(tag: String? = null, listener: DownloadListener)
/**
* 移除回调监听
* @param tag 标签
*/
fun removeListener(tag: String)
/**
* 移除回调监听
* @param listener 监听器
*/
fun removeListener(listener: DownloadListener)
}

View File

@@ -1,4 +0,0 @@
package com.chwl.library.download
open class FileDownloadListener : DownloadListener

View File

@@ -1,47 +0,0 @@
package com.chwl.library.download
open class FileDownloadRequest(
// 下载地址
private val url: String,
// 保存路径(完整)
private val path: String,
// 别名
private val tag: String? = null,
// 请求头
private val header: Map<String, String>? = null,
// 下载超时时间
private val timeout: Long? = null,
) : DownloadRequest {
override fun getUrl(): String {
return url
}
override fun getPath(): String {
return path
}
override fun getTag(): String {
return tag ?: "DEFAULT_TAG"
}
override fun getHeader(): Map<String, String>? {
return header
}
override fun createTask(): DownloadTask {
return FileDownloadTask(this)
}
override fun getTimeout(): Long? {
return timeout
}
override fun equalsTarget(request: DownloadRequest): Boolean {
// 暂时只判断目标地址是否一致
return request.getUrl() == getUrl()
}
}

View File

@@ -1,276 +0,0 @@
package com.chwl.library.download
import android.os.Handler
import android.os.Looper
import com.example.lib_utils.FileUtils2
import com.example.lib_utils.log.ILog
import com.liulishuo.filedownloader.BaseDownloadTask
import com.liulishuo.filedownloader.FileDownloadListener
import com.liulishuo.filedownloader.FileDownloader
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeoutException
open class FileDownloadTask(
private val request: DownloadRequest,
) : DownloadTask, ILog {
// 真正执行下载工作的任务第三方组件SDK
private var task: BaseDownloadTask? = null
// 工作的任务监听器
private var taskListener: ListenerBridge? = null
// 监听器列表 <标签,监听器>
private val listeners: ConcurrentHashMap<String, DownloadListener> by lazy {
ConcurrentHashMap()
}
// 任务ID
private val taskId: String by lazy {
getRequest().getUrl() + getRequest().getPath()
}
// 状态
@Volatile
private var state: Int = DownloadState.IDLE
private var handler: Handler? = null
// 是否释放了(临时记录,只是为了协助排除问题)
private var isCleared: Boolean? = null
override fun getRequest(): DownloadRequest {
return request
}
override fun getState(): Int {
return state
}
override fun getId(): String {
return taskId
}
override fun getFilePath(): String {
return getWorkTask().targetFilePath
}
override fun start() {
logI(
"start()" +
"\nurl:${getRequest().getUrl()}" +
"\npath:${getRequest().getPath()}" +
"\ntimeout:${getRequest().getTimeout()}" +
"\nstate:${getState()}" +
"\nworkTask-status:${getWorkTask().status}", filePrinter = true
)
if (FileUtils2.isFileExists(getRequest().getPath())) {
logD("该目标文件已存在本地")
downloadSuccess()
return
}
if (isCleared == true) {
logE("当前任务已释放 url:${request.getUrl()}", filePrinter = true)
return
}
if (getState() == DownloadState.IDLE || getState() == DownloadState.ERROR) {
state = DownloadState.STARTED
startTimeout()
// -------start
// TODO 线上部分异常调用start时内部抛正在进行的异常(This task is dirty to restart, If you want to reuse this task, please invoke #reuse method manually and retry to restart again)
// TODO 临时预先判断记录下
if (getWorkTask().isUsing) {
logE("下载正在进行无需重复启动url:${request.getUrl()}", filePrinter = true)
return
}
// -------end
try {
getWorkTask().start()
} catch (e: Exception) {
downloadError(e)
}
} else {
logI("start() 已经启动")
}
}
override fun stop() {
logI("stop()", filePrinter = true)
if (getState() != DownloadState.PAUSED && getState() != DownloadState.IDLE) {
getWorkTask().pause()
}
onCleared()
}
override fun addListener(tag: String?, listener: DownloadListener) {
listeners[tag ?: getRequest().getTag()] = listener
}
override fun removeListener(tag: String) {
listeners.remove(tag)
if (listeners.isEmpty()) {
stop()
}
}
override fun removeListener(listener: DownloadListener) {
listeners.forEach {
if (it == listener) {
listeners.remove(it.key)
if (listeners.isEmpty()) {
stop()
}
return
}
}
}
/**
* (真正)工作的任务
*/
protected open fun getWorkTask(): BaseDownloadTask {
if (task == null) {
task = createTask()
}
return task!!
}
/**
* 创建任务
*/
protected open fun createTask(): BaseDownloadTask {
taskListener = ListenerBridge()
return FileDownloader.getImpl().create(getRequest().getUrl()).apply {
setPath(getRequest().getPath(), false)
this.listener = taskListener
}
}
/**
* 释放工作任务相关资源
*/
private fun releaseWorkTask() {
task?.listener = null
taskListener = null
task = null
}
/**
* 释放
*/
override fun onCleared() {
super.onCleared()
logD("onCleared() url:${getRequest().getUrl()}", filePrinter = true)
this.isCleared = true
// 从任务管理中移除自己
DownloadManager.removeTask(this)
handler?.removeCallbacksAndMessages(null)
handler = null
// 释放工作任务
releaseWorkTask()
// 移除所有监听
listeners.clear()
}
/**
* 下载成功处理
*/
private fun downloadSuccess() {
logI("downloadSuccess() url:${getRequest().getUrl()}", filePrinter = true)
state = DownloadState.SUCCESS
listeners.values.forEach {
it.onDownloadCompleted(this)
}
onCleared()
}
/**
* 下载失败
*/
private fun downloadError(error: Throwable) {
logE("downloadError() url:${getRequest().getUrl()}", error, filePrinter = true)
state = DownloadState.ERROR
listeners.values.forEach {
it.onDownloadError(DownloadException(this@FileDownloadTask, error))
}
onCleared()
}
/**
* 开始超时检测
*/
private fun startTimeout() {
val timeout = request.getTimeout()
if (timeout == null || timeout < 500) {
// 时间太小就过滤掉
return
}
logD("startTimeout() timeout:$timeout")
if (handler == null) {
handler = Handler(Looper.getMainLooper())
} else {
handler?.removeCallbacksAndMessages(null)
}
handler?.postDelayed({
logD("startTimeout() postDelayed state:$state")
if (state != DownloadState.IDLE && state != DownloadState.SUCCESS && state != DownloadState.ERROR) {
logI(
"startTimeout() postDelayed state:$state url:${getRequest().getUrl()}",
filePrinter = true
)
if (getState() != DownloadState.PAUSED) {
getWorkTask().pause()
}
downloadError(TimeoutException())
}
}, timeout)
}
/**
* 监听器转发处理
*/
private inner class ListenerBridge : FileDownloadListener() {
override fun pending(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
state = DownloadState.STARTED
logI(
"pending() url:${getRequest().getUrl()} soFarBytes:$soFarBytes,totalBytes:$totalBytes",
filePrinter = true
)
}
override fun progress(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
state = DownloadState.PROGRESS
// logD("progress() soFarBytes:$soFarBytes,totalBytes:$totalBytes")
listeners.values.forEach {
it.onDownloadProgress(this@FileDownloadTask, soFarBytes, totalBytes)
}
}
/**
* 完成时
*/
override fun completed(task: BaseDownloadTask) {
downloadSuccess()
}
override fun paused(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
logI("paused() url:${getRequest().getUrl()}", filePrinter = true)
state = DownloadState.PAUSED
}
override fun error(task: BaseDownloadTask, e: Throwable) {
downloadError(e)
}
/**
* 在下载队列中(正在等待/正在下载)已经存在相同下载连接与相同存储路径的任务
*/
override fun warn(task: BaseDownloadTask) {
logI("warn() 已经存在相同下载连接与相同存储路径的任务 url:${getRequest().getUrl()}", filePrinter = true)
}
}
}

View File

@@ -3,12 +3,11 @@ package com.chwl.library.widget
import android.content.Context
import android.util.AttributeSet
import android.util.LruCache
import com.chwl.library.download.DownloadException
import com.chwl.library.download.DownloadManager
import com.chwl.library.download.DownloadManager.download
import com.chwl.library.download.DownloadRequest.Companion.build
import com.chwl.library.download.DownloadTask
import com.chwl.library.download.FileDownloadListener
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.chwl.library.common.glide.GlideUtils
import com.chwl.library.utils.LogUtil
import com.chwl.library.utils.PathHelper
import com.example.lib_utils.AppUtils
@@ -19,6 +18,7 @@ import com.opensource.svgaplayer.SVGAParser
import com.opensource.svgaplayer.SVGAParser.Companion.shareParser
import com.opensource.svgaplayer.SVGAVideoEntity
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.net.MalformedURLException
@@ -48,7 +48,6 @@ class SVGAView : SVGAImageView, ILog {
private var logTag: String? = null
private var isDownloading = false
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
@@ -83,10 +82,6 @@ class SVGAView : SVGAImageView, ILog {
this.resourceUrl = null
this.setImageDrawable(null)
onViewStateChanged(0)
if (isDownloading) {
DownloadManager.stopTag(downloadTag)
isDownloading = false
}
return
}
if (url == resourceUrl && drawable is SVGADrawable) {
@@ -110,28 +105,38 @@ class SVGAView : SVGAImageView, ILog {
private fun loadSVGAUrl(url: String) {
logD("loadSVGAUrl url:$url")
DownloadManager.stopTag(downloadTag)
val filePath = PathHelper.generateResourcesFilePath(url)
val request = build(url, filePath, downloadTag, null, 60000L)
this.isDownloading = true
download(request, object : FileDownloadListener() {
override fun onDownloadCompleted(task: DownloadTask) {
this@SVGAView.isDownloading = false
val path = task.getRequest().getPath()
logD("loadSVGAUrl onDownloadCompleted url:$url path:$path")
if (resourceUrl == url) {
loadSVGAFile(url, path)
}
}
override fun onDownloadError(exception: DownloadException) {
this@SVGAView.isDownloading = false
GlideUtils.instance().downloadFromUrl(context,url,object : RequestListener<File?> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File?>?,
isFirstResource: Boolean
): Boolean {
logD("loadSVGAUrl onDownloadError url:$url")
if (resourceUrl == url) {
onViewStateChanged(-1)
}
return true
}
override fun onResourceReady(
resource: File?,
model: Any?,
target: Target<File?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
if (resource != null) {
val path = resource.path
logD("loadSVGAUrl onDownloadCompleted url:$url path:$path")
if (resourceUrl == url) {
loadSVGAFile(url, path)
}
}
return true
}
})
}
private fun loadSVGAFile(url: String, path: String) {