Remove all AsyncTask usage, Resolve #631
This commit is contained in:
1 parent
cac88cbb81
commit
019174d7e2
3 files changed
+160
-393
No files matched your search
@@ -13,106 +13,62 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
package com.hippo.ehviewer.preference;
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
private val NO_REDUNDANCY =
|
||||
GetText.getString(R.string.settings_download_clean_redundancy_no_redundancy)
|
||||
private val CLEAR_REDUNDANCY_DONE =
|
||||
{ cnt: Int -> GetText.getString(R.string.settings_download_clean_redundancy_done, cnt) }
|
||||
private val FINAL_CLEAR_REDUNDANCY_MSG =
|
||||
{ cnt: Int -> if (cnt == 0) NO_REDUNDANCY else CLEAR_REDUNDANCY_DONE(cnt) }
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.download.DownloadManager;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
public class CleanRedundancyPreference extends TaskPreference {
|
||||
|
||||
public CleanRedundancyPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CleanRedundancyPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CleanRedundancyPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Task onCreateTask() {
|
||||
return new ClearTask(getContext());
|
||||
}
|
||||
|
||||
private static class ClearTask extends Task {
|
||||
|
||||
private final DownloadManager mManager;
|
||||
|
||||
public ClearTask(@NonNull Context context) {
|
||||
super(context);
|
||||
mManager = EhApplication.getDownloadManager();
|
||||
class CleanRedundancyPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : TaskPreference(context, attrs) {
|
||||
private fun clearFile(file: UniFile): Boolean {
|
||||
var name = file.name ?: return false
|
||||
val index = name.indexOf('-')
|
||||
if (index >= 0) {
|
||||
name = name.substring(0, index)
|
||||
}
|
||||
|
||||
// True for cleared
|
||||
private boolean clearFile(UniFile file) {
|
||||
String name = file.getName();
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
int index = name.indexOf('-');
|
||||
if (index >= 0) {
|
||||
name = name.substring(0, index);
|
||||
}
|
||||
long gid = NumberUtils.parseLongSafely(name, -1L);
|
||||
if (-1L == gid) {
|
||||
return false;
|
||||
}
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
return false;
|
||||
}
|
||||
file.delete();
|
||||
return true;
|
||||
val gid = name.toLongOrNull() ?: return false
|
||||
if (downloadManager.containDownloadInfo(gid)) {
|
||||
return false
|
||||
}
|
||||
file.delete()
|
||||
return true
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doInBackground(Void... params) {
|
||||
UniFile dir = Settings.getDownloadLocation();
|
||||
if (null == dir) {
|
||||
return 0;
|
||||
}
|
||||
UniFile[] files = dir.listFiles();
|
||||
if (null == files) {
|
||||
return 0;
|
||||
}
|
||||
private fun doRealWork(): Int {
|
||||
return Settings.getDownloadLocation()?.listFiles()?.sumOf { clearFile(it).compareTo(false) } ?: 0
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (UniFile f : files) {
|
||||
if (clearFile(f)) {
|
||||
++count;
|
||||
}
|
||||
override fun launchJob() {
|
||||
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
|
||||
launchUI {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
int count;
|
||||
if (o instanceof Integer) {
|
||||
count = (Integer) o;
|
||||
} else {
|
||||
count = 0;
|
||||
else singletonJob = launch {
|
||||
val cnt = doRealWork()
|
||||
withUIContext {
|
||||
showTip(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
showTip(0 == count ?
|
||||
mApplication.getString(R.string.settings_download_clean_redundancy_no_redundancy) :
|
||||
mApplication.getString(R.string.settings_download_clean_redundancy_done, count), Snackbar.LENGTH_SHORT);
|
||||
super.onPostExecute(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var singletonJob: Job? = null
|
||||
@@ -15,145 +15,111 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine.fillGalleryListByApi
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.readCompatFromUniFile
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.IOException
|
||||
import java.util.Collections
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
class RestoreDownloadPreference constructor(
|
||||
context: Context, attrs: AttributeSet?
|
||||
private val RESTORE_NOT_FOUND = GetText.getString(R.string.settings_download_restore_not_found)
|
||||
private val RESTORE_FAILED = GetText.getString(R.string.settings_download_restore_failed)
|
||||
private val RESTORE_COUNT_MSG =
|
||||
{ cnt: Int -> if (cnt == 0) RESTORE_NOT_FOUND else GetText.getString(R.string.settings_download_restore_successfully, cnt) }
|
||||
|
||||
class RestoreDownloadPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : TaskPreference(context, attrs) {
|
||||
override fun onCreateTask(): Task {
|
||||
return RestoreTask(context)
|
||||
}
|
||||
|
||||
private class RestoreTask(context: Context) : Task(context) {
|
||||
private val mManager: DownloadManager = downloadManager
|
||||
private var restoreDirCount = 0
|
||||
private fun getRestoreItem(file: UniFile?): RestoreItem? {
|
||||
if (null == file || !file.isDirectory) {
|
||||
private val mManager: DownloadManager = downloadManager
|
||||
private var restoreDirCount = 0
|
||||
private fun getRestoreItem(file: UniFile?): RestoreItem? {
|
||||
if (null == file || !file.isDirectory) return null
|
||||
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
|
||||
return runCatching {
|
||||
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
|
||||
var gid = spiderInfo.gid
|
||||
var dirname = file.name
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
// Restore download dir to avoid redownload
|
||||
val dbdirname = EhDB.getDownloadDirname(gid)
|
||||
if (null == dbdirname || dirname != dbdirname) {
|
||||
EhDB.putDownloadDirname(gid, dirname)
|
||||
restoreDirCount++
|
||||
}
|
||||
return null
|
||||
}
|
||||
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
|
||||
return try {
|
||||
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
|
||||
val gid = spiderInfo.gid
|
||||
val dirname = file.name
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
// Restore download dir to avoid redownload
|
||||
val dbdirname = EhDB.getDownloadDirname(gid)
|
||||
if (null == dbdirname || dirname != dbdirname) {
|
||||
EhDB.putDownloadDirname(gid, dirname)
|
||||
restoreDirCount++
|
||||
}
|
||||
return null
|
||||
}
|
||||
val token = spiderInfo.token
|
||||
val restoreItem = RestoreItem()
|
||||
restoreItem.gid = gid
|
||||
restoreItem.token = token
|
||||
restoreItem.dirname = dirname
|
||||
restoreItem
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
RestoreItem().apply {
|
||||
gid = spiderInfo.gid
|
||||
token = spiderInfo.token
|
||||
dirname = file.name
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg params: Void): Any? {
|
||||
val dir = Settings.getDownloadLocation() ?: return null
|
||||
val restoreItemList: MutableList<RestoreItem> = ArrayList()
|
||||
val files = dir.listFiles() ?: return null
|
||||
for (file in files) {
|
||||
val restoreItem = getRestoreItem(file)
|
||||
if (null != restoreItem) {
|
||||
restoreItemList.add(restoreItem)
|
||||
}
|
||||
}
|
||||
return if (0 == restoreItemList.size) {
|
||||
Collections.EMPTY_LIST
|
||||
} else try {
|
||||
runBlocking {
|
||||
fillGalleryListByApi(
|
||||
ArrayList(restoreItemList),
|
||||
EhUrl.referer
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostExecute(o: Any) {
|
||||
if (o !is List<*>) {
|
||||
mActivity.showTip(R.string.settings_download_restore_failed, BaseScene.LENGTH_SHORT)
|
||||
} else {
|
||||
val list = o as List<RestoreItem>
|
||||
if (list.isEmpty()) {
|
||||
mActivity.showTip(
|
||||
if (restoreDirCount == 0)
|
||||
mActivity.getString(R.string.settings_download_restore_not_found)
|
||||
else
|
||||
mActivity.getString(
|
||||
R.string.settings_download_restore_successfully,
|
||||
restoreDirCount
|
||||
),
|
||||
BaseScene.LENGTH_SHORT
|
||||
)
|
||||
} else {
|
||||
var count = 0
|
||||
var i = 0
|
||||
val n = list.size
|
||||
while (i < n) {
|
||||
val item = list[i]
|
||||
// Avoid failed gallery info
|
||||
if (null != item.title) {
|
||||
// Put to download
|
||||
mManager.addDownload(item, null)
|
||||
// Put download dir to DB
|
||||
EhDB.putDownloadDirname(item.gid, item.dirname)
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
showTip(
|
||||
mActivity.getString(
|
||||
R.string.settings_download_restore_successfully,
|
||||
count + restoreDirCount
|
||||
),
|
||||
BaseScene.LENGTH_SHORT
|
||||
)
|
||||
val preference: Preference? = preference
|
||||
if (null != preference) {
|
||||
val context = preference.context
|
||||
if (context is Activity) {
|
||||
context.setResult(Activity.RESULT_OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onPostExecute(o)
|
||||
}
|
||||
private suspend fun doRealWork(): List<GalleryInfo>? {
|
||||
val dir = Settings.getDownloadLocation() ?: return null
|
||||
val files = dir.listFiles() ?: return null
|
||||
val restoreItemList = files.mapNotNull { getRestoreItem(it) }
|
||||
return runSuspendCatching {
|
||||
fillGalleryListByApi(restoreItemList, EhUrl.referer)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private class RestoreItem : BaseGalleryInfo() {
|
||||
var dirname: String? = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun launchJob() {
|
||||
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
|
||||
launchUI {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
else singletonJob = launch {
|
||||
val result = doRealWork()
|
||||
withUIContext {
|
||||
if (result == null) {
|
||||
showTip(RESTORE_FAILED)
|
||||
} else {
|
||||
if (result.isEmpty()) {
|
||||
showTip(RESTORE_COUNT_MSG(restoreDirCount))
|
||||
} else {
|
||||
var count = 0
|
||||
var i = 0
|
||||
val n = result.size
|
||||
while (i < n) {
|
||||
val item = result[i]
|
||||
// Avoid failed gallery info
|
||||
if (null != item.title) {
|
||||
// Put to download
|
||||
mManager.addDownload(item, null)
|
||||
// Put download dir to DB
|
||||
EhDB.putDownloadDirname(item.gid, (item as RestoreItem).dirname)
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
showTip(RESTORE_COUNT_MSG(count + restoreDirCount))
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var singletonJob: Job? = null
|
||||
@@ -13,196 +13,41 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
package com.hippo.ehviewer.preference;
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.ui.SettingsActivity
|
||||
import com.hippo.preference.DialogPreference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.customview.view.AbsSavedState;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.ui.SettingsActivity;
|
||||
import com.hippo.preference.DialogPreference;
|
||||
import com.hippo.util.IoThreadPoolExecutor;
|
||||
import com.hippo.yorozuya.IntIdGenerator;
|
||||
|
||||
public abstract class TaskPreference extends DialogPreference {
|
||||
|
||||
private Task mTask;
|
||||
private int mTaskId = IntIdGenerator.INVALID_ID;
|
||||
|
||||
public TaskPreference(Context context) {
|
||||
super(context);
|
||||
abstract class TaskPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : DialogPreference(context, attrs), CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
protected lateinit var dialog: AlertDialog
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
builder.setTitle(null)
|
||||
builder.setView(R.layout.preference_dialog_task)
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
public TaskPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
abstract fun launchJob()
|
||||
|
||||
override fun onDialogCreated(dialog: AlertDialog) {
|
||||
this.dialog = dialog
|
||||
launchJob()
|
||||
}
|
||||
|
||||
public TaskPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
builder.setTitle(null);
|
||||
builder.setView(R.layout.preference_dialog_task);
|
||||
builder.setCancelable(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogCreated(AlertDialog dialog) {
|
||||
if (null == mTask) {
|
||||
mTask = onCreateTask();
|
||||
mTask.setPreference(this);
|
||||
mTaskId = ((EhApplication) getContext().getApplicationContext()).putGlobalStuff(mTask);
|
||||
mTask.executeOnExecutor(IoThreadPoolExecutor.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
protected void onTaskEnd() {
|
||||
// Dismiss dialog
|
||||
Dialog dialog = getDialog();
|
||||
try {
|
||||
dialog.dismiss();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// Clear async
|
||||
mTask = null;
|
||||
mTaskId = IntIdGenerator.INVALID_ID;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected abstract Task onCreateTask();
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState myState = new SavedState(superState);
|
||||
myState.asyncTaskId = mTaskId;
|
||||
return myState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
if (state == null || !state.getClass().equals(SavedState.class)) {
|
||||
// Didn't save state for us in onSaveInstanceState
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
SavedState myState = (SavedState) state;
|
||||
mTaskId = myState.asyncTaskId;
|
||||
if (IntIdGenerator.INVALID_ID != mTaskId) {
|
||||
Object o = ((EhApplication) getContext().getApplicationContext()).getGlobalStuff(mTaskId);
|
||||
if (o instanceof Task) {
|
||||
mTask = (Task) o;
|
||||
mTask.setPreference(this);
|
||||
}
|
||||
}
|
||||
if (null == mTask) {
|
||||
mTaskId = IntIdGenerator.INVALID_ID;
|
||||
}
|
||||
|
||||
// TODO if not task, show not reopen dialog
|
||||
|
||||
super.onRestoreInstanceState(myState.getSuperState());
|
||||
}
|
||||
|
||||
private static class SavedState extends AbsSavedState {
|
||||
public static final Parcelable.Creator<SavedState> CREATOR =
|
||||
new Parcelable.Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
int asyncTaskId;
|
||||
|
||||
public SavedState(Parcel source) {
|
||||
super(source, SavedState.class.getClassLoader());
|
||||
asyncTaskId = source.readInt();
|
||||
}
|
||||
|
||||
public SavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(asyncTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class Task extends AsyncTask<Void, Void, Object> {
|
||||
|
||||
protected final EhApplication mApplication;
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
protected final SettingsActivity mActivity;
|
||||
@Nullable
|
||||
private TaskPreference mPreference;
|
||||
|
||||
public Task(@NonNull Context context) {
|
||||
mActivity = (SettingsActivity) context;
|
||||
mApplication = (EhApplication) context.getApplicationContext();
|
||||
}
|
||||
|
||||
public EhApplication getApplication() {
|
||||
return mApplication;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TaskPreference getPreference() {
|
||||
return mPreference;
|
||||
}
|
||||
|
||||
public void setPreference(@Nullable TaskPreference preference) {
|
||||
mPreference = preference;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
mApplication.removeGlobalStuff(this);
|
||||
if (null != mPreference) {
|
||||
mPreference.onTaskEnd();
|
||||
}
|
||||
}
|
||||
|
||||
protected void showTip(@StringRes int id, int length) {
|
||||
try {
|
||||
mActivity.showTip(id, length);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(mApplication, id, length).show();
|
||||
}
|
||||
}
|
||||
|
||||
protected void showTip(CharSequence message, int length) {
|
||||
try {
|
||||
mActivity.showTip(message, length);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(mApplication, message, length).show();
|
||||
}
|
||||
}
|
||||
protected fun showTip(msg: String) {
|
||||
(context as SettingsActivity).showTip(
|
||||
msg,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user