mirror of
https://github.com/TwidereProject/Twidere-Android
synced 2025-02-02 17:56:56 +01:00
improving extension
This commit is contained in:
parent
ca20ca865e
commit
d4ca49ccdc
@ -18,12 +18,10 @@
|
||||
*/
|
||||
package org.mariotaku.twidere;
|
||||
|
||||
import org.mariotaku.twidere.model.MediaUploadResult;
|
||||
import org.mariotaku.twidere.model.ParcelableStatusUpdate;
|
||||
import org.mariotaku.twidere.model.UploaderMediaItem;
|
||||
|
||||
interface IMediaUploader {
|
||||
|
||||
MediaUploadResult upload(in ParcelableStatusUpdate status, in UploaderMediaItem[] media);
|
||||
|
||||
String upload(String statusJson, String mediaJson);
|
||||
|
||||
boolean callback(String resultJson, String statusJson);
|
||||
|
||||
}
|
||||
|
@ -18,11 +18,9 @@
|
||||
*/
|
||||
package org.mariotaku.twidere;
|
||||
|
||||
import org.mariotaku.twidere.model.Account;
|
||||
|
||||
interface ITimelineSyncHelper {
|
||||
|
||||
boolean put(in Account account, String key, long value);
|
||||
boolean put(long accountId, String key, long value);
|
||||
|
||||
long get(in Account account, String key);
|
||||
long get(long accountId, String key);
|
||||
}
|
||||
|
@ -3,20 +3,30 @@ package org.mariotaku.twidere.model;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField;
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject;
|
||||
import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease;
|
||||
import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@ParcelablePlease
|
||||
@JsonObject
|
||||
public class MediaUploadResult implements Parcelable {
|
||||
|
||||
@ParcelableThisPlease
|
||||
@JsonField(name = "media_uris")
|
||||
public String[] media_uris;
|
||||
@ParcelableThisPlease
|
||||
@JsonField(name = "error_code")
|
||||
public int error_code;
|
||||
@ParcelableThisPlease
|
||||
@JsonField(name = "error_message")
|
||||
public String error_message;
|
||||
@ParcelableThisPlease
|
||||
@JsonField(name = "extras")
|
||||
public String extras;
|
||||
|
||||
public static final Creator<MediaUploadResult> CREATOR = new Creator<MediaUploadResult>() {
|
||||
public MediaUploadResult createFromParcel(Parcel source) {
|
||||
MediaUploadResult target = new MediaUploadResult();
|
||||
@ -29,6 +39,9 @@ public class MediaUploadResult implements Parcelable {
|
||||
}
|
||||
};
|
||||
|
||||
MediaUploadResult() {
|
||||
}
|
||||
|
||||
public MediaUploadResult(final int errorCode, final String errorMessage) {
|
||||
if (errorCode == 0) throw new IllegalArgumentException("Error code must not be 0");
|
||||
media_uris = null;
|
||||
@ -36,9 +49,6 @@ public class MediaUploadResult implements Parcelable {
|
||||
error_message = errorMessage;
|
||||
}
|
||||
|
||||
MediaUploadResult() {
|
||||
}
|
||||
|
||||
public MediaUploadResult(final String[] mediaUris) {
|
||||
if (mediaUris == null) throw new IllegalArgumentException("Media uris must not be null");
|
||||
media_uris = mediaUris;
|
||||
@ -46,14 +56,6 @@ public class MediaUploadResult implements Parcelable {
|
||||
error_message = null;
|
||||
}
|
||||
|
||||
public static MediaUploadResult getInstance(final int errorCode, final String errorMessage) {
|
||||
return new MediaUploadResult(errorCode, errorMessage);
|
||||
}
|
||||
|
||||
public static MediaUploadResult getInstance(final String... mediaUris) {
|
||||
return new MediaUploadResult(mediaUris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MediaUploadResult{media_uris=" + Arrays.toString(media_uris) + ", error_code=" + error_code
|
||||
@ -69,4 +71,12 @@ public class MediaUploadResult implements Parcelable {
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
MediaUploadResultParcelablePlease.writeToParcel(this, dest, flags);
|
||||
}
|
||||
|
||||
public static MediaUploadResult getInstance(final int errorCode, final String errorMessage) {
|
||||
return new MediaUploadResult(errorCode, errorMessage);
|
||||
}
|
||||
|
||||
public static MediaUploadResult getInstance(final String... mediaUris) {
|
||||
return new MediaUploadResult(mediaUris);
|
||||
}
|
||||
}
|
||||
|
@ -28,14 +28,13 @@ public class TwitLongerReaderActivity extends Activity implements Constants, OnC
|
||||
private ParcelableStatus mStatus;
|
||||
private TwitLongerReaderTask mTwitLongerPostTask;
|
||||
private static final Pattern PATTERN_TWITLONGER = Pattern.compile(
|
||||
"((tl\\.gd|www.twitlonger.com\\/show)\\/([\\w\\d]+))", Pattern.CASE_INSENSITIVE);
|
||||
"((tl\\.gd|www.twitlonger.com/show)/([\\w\\d]+))", Pattern.CASE_INSENSITIVE);
|
||||
private static final int GROUP_TWITLONGER_ID = 3;
|
||||
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
switch (view.getId()) {
|
||||
case R.id.action: {
|
||||
|
||||
if (mResult == null) {
|
||||
if (mStatus == null) return;
|
||||
if (mTwitLongerPostTask != null) {
|
||||
@ -43,11 +42,11 @@ public class TwitLongerReaderActivity extends Activity implements Constants, OnC
|
||||
}
|
||||
final Matcher m = PATTERN_TWITLONGER.matcher(mStatus.text_html);
|
||||
if (m.find()) {
|
||||
mTwitLongerPostTask = new TwitLongerReaderTask(m.group(GROUP_TWITLONGER_ID));
|
||||
mTwitLongerPostTask.execute();
|
||||
mTwitLongerPostTask = new TwitLongerReaderTask(this);
|
||||
mTwitLongerPostTask.execute(m.group(GROUP_TWITLONGER_ID));
|
||||
}
|
||||
} else {
|
||||
if (mUser == null || mResult == null) return;
|
||||
if (mUser == null) return;
|
||||
final Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, "@" + mUser + ": " + mResult);
|
||||
@ -89,8 +88,8 @@ public class TwitLongerReaderActivity extends Activity implements Constants, OnC
|
||||
if (mTwitLongerPostTask != null) {
|
||||
mTwitLongerPostTask.cancel(true);
|
||||
}
|
||||
mTwitLongerPostTask = new TwitLongerReaderTask(m.group(GROUP_TWITLONGER_ID));
|
||||
mTwitLongerPostTask.execute();
|
||||
mTwitLongerPostTask = new TwitLongerReaderTask(this);
|
||||
mTwitLongerPostTask.execute(m.group(GROUP_TWITLONGER_ID));
|
||||
} else {
|
||||
finish();
|
||||
return;
|
||||
@ -109,12 +108,12 @@ public class TwitLongerReaderActivity extends Activity implements Constants, OnC
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
public final class TwitLongerReaderTask extends AsyncTask<String, Object, TaskResponse<Post, TwitLongerException>> {
|
||||
public static class TwitLongerReaderTask extends AsyncTask<String, Object, TaskResponse<Post, TwitLongerException>> {
|
||||
|
||||
private final String id;
|
||||
private final TwitLongerReaderActivity activity;
|
||||
|
||||
public TwitLongerReaderTask(final String id) {
|
||||
this.id = id;
|
||||
public TwitLongerReaderTask(TwitLongerReaderActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -129,23 +128,37 @@ public class TwitLongerReaderActivity extends Activity implements Constants, OnC
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final TaskResponse<Post, TwitLongerException> result) {
|
||||
mProgress.setVisibility(View.GONE);
|
||||
mActionButton.setVisibility(View.VISIBLE);
|
||||
if (result.hasError()) {
|
||||
mActionButton.setImageResource(R.drawable.ic_menu_send);
|
||||
activity.showError(result.getThrowable());
|
||||
} else {
|
||||
mActionButton.setImageResource(R.drawable.ic_menu_share);
|
||||
activity.showResult(result.getObject());
|
||||
}
|
||||
|
||||
super.onPostExecute(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
mProgress.setVisibility(View.VISIBLE);
|
||||
mActionButton.setVisibility(View.GONE);
|
||||
super.onPreExecute();
|
||||
activity.showProgress();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void showProgress() {
|
||||
mProgress.setVisibility(View.VISIBLE);
|
||||
mActionButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void showResult(Post post) {
|
||||
mProgress.setVisibility(View.GONE);
|
||||
mActionButton.setVisibility(View.VISIBLE);
|
||||
mActionButton.setImageResource(R.drawable.ic_menu_share);
|
||||
mPreview.setText(post.content);
|
||||
}
|
||||
|
||||
private void showError(TwitLongerException e) {
|
||||
mProgress.setVisibility(View.GONE);
|
||||
mActionButton.setVisibility(View.VISIBLE);
|
||||
mActionButton.setImageResource(R.drawable.ic_menu_send);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,87 @@
|
||||
package org.mariotaku.twidere.service;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import org.mariotaku.twidere.IMediaUploader;
|
||||
import org.mariotaku.twidere.model.MediaUploadResult;
|
||||
import org.mariotaku.twidere.model.ParcelableStatus;
|
||||
import org.mariotaku.twidere.model.ParcelableStatusUpdate;
|
||||
import org.mariotaku.twidere.model.UploaderMediaItem;
|
||||
import org.mariotaku.twidere.util.LoganSquareMapperFinder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abstract media uploader service
|
||||
* <p/>
|
||||
* Created by mariotaku on 16/2/27.
|
||||
*/
|
||||
public abstract class MediaUploaderService extends Service {
|
||||
|
||||
private final MediaUploaderStub mBinder = new MediaUploaderStub(this);
|
||||
|
||||
public final IBinder onBind(final Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
protected abstract MediaUploadResult upload(ParcelableStatusUpdate status,
|
||||
UploaderMediaItem[] media);
|
||||
|
||||
protected abstract boolean callback(MediaUploadResult result, ParcelableStatus status);
|
||||
|
||||
/*
|
||||
* By making this a static class with a WeakReference to the Service, we
|
||||
* ensure that the Service can be GCd even when the system process still has
|
||||
* a remote reference to the stub.
|
||||
*/
|
||||
private static final class MediaUploaderStub extends IMediaUploader.Stub {
|
||||
|
||||
final WeakReference<MediaUploaderService> mService;
|
||||
|
||||
public MediaUploaderStub(final MediaUploaderService service) {
|
||||
mService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(String statusJson, String mediaJson) throws RemoteException {
|
||||
try {
|
||||
final ParcelableStatusUpdate statusUpdate = LoganSquareMapperFinder.mapperFor(ParcelableStatusUpdate.class)
|
||||
.parse(statusJson);
|
||||
final List<UploaderMediaItem> media = LoganSquareMapperFinder.mapperFor(UploaderMediaItem.class)
|
||||
.parseList(mediaJson);
|
||||
final MediaUploadResult shorten = mService.get().upload(statusUpdate, media.toArray(new UploaderMediaItem[media.size()]));
|
||||
return LoganSquareMapperFinder.mapperFor(MediaUploadResult.class).serialize(shorten);
|
||||
} catch (IOException e) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
|
||||
throw new RemoteException(e.getMessage());
|
||||
} else {
|
||||
throw new RemoteException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean callback(String resultJson, String statusJson) throws RemoteException {
|
||||
try {
|
||||
final MediaUploadResult result = LoganSquareMapperFinder.mapperFor(MediaUploadResult.class)
|
||||
.parse(resultJson);
|
||||
final ParcelableStatus status = LoganSquareMapperFinder.mapperFor(ParcelableStatus.class)
|
||||
.parse(statusJson);
|
||||
return mService.get().callback(result, status);
|
||||
} catch (IOException e) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
|
||||
throw new RemoteException(e.getMessage());
|
||||
} else {
|
||||
throw new RemoteException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -16,12 +16,14 @@ import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Abstract status shortener service
|
||||
* <p/>
|
||||
* Created by mariotaku on 16/2/20.
|
||||
*/
|
||||
public abstract class StatusShortenerService extends Service {
|
||||
private final StatusShortenerStub mBinder = new StatusShortenerStub(this);
|
||||
|
||||
public IBinder onBind(final Intent intent) {
|
||||
public final IBinder onBind(final Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
|
@ -518,6 +518,23 @@ public class BackgroundOperationService extends IntentService implements Constan
|
||||
if (uploader == null) {
|
||||
throw new UploaderNotFoundException(getString(R.string.error_message_media_uploader_not_found));
|
||||
}
|
||||
try {
|
||||
uploader.checkService(new AbsServiceInterface.CheckServiceAction() {
|
||||
@Override
|
||||
public void check(@Nullable Bundle metaData) throws AbsServiceInterface.CheckServiceException {
|
||||
if (metaData == null) throw new ExtensionVersionMismatchException();
|
||||
final String extensionVersion = metaData.getString(METADATA_KEY_EXTENSION_VERSION_MEDIA_UPLOADER);
|
||||
if (!TextUtils.equals(extensionVersion, getString(R.string.media_uploader_service_interface_version))) {
|
||||
throw new ExtensionVersionMismatchException();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (AbsServiceInterface.CheckServiceException e) {
|
||||
if (e instanceof ExtensionVersionMismatchException) {
|
||||
throw new UploadException(getString(R.string.uploader_version_incompatible));
|
||||
}
|
||||
throw new UploadException(e);
|
||||
}
|
||||
}
|
||||
if (!ServicePickerPreference.isNoneValue(shortenerComponent)) {
|
||||
shortener = StatusShortenerInterface.getInstance(app, shortenerComponent);
|
||||
@ -577,8 +594,8 @@ public class BackgroundOperationService extends IntentService implements Constan
|
||||
String statusText = statusUpdate.text;
|
||||
|
||||
// Use custom uploader to upload media
|
||||
MediaUploadResult uploadResult = null;
|
||||
if (uploader != null && hasMedia) {
|
||||
final MediaUploadResult uploadResult;
|
||||
try {
|
||||
uploadResult = uploader.upload(statusUpdate,
|
||||
UploaderMediaItem.getFromStatusUpdate(this, statusUpdate));
|
||||
@ -685,6 +702,9 @@ public class BackgroundOperationService extends IntentService implements Constan
|
||||
if (shouldShorten && shortener != null && shortenedResult != null) {
|
||||
shortener.callback(shortenedResult, result);
|
||||
}
|
||||
if (uploader != null && uploadResult != null) {
|
||||
uploader.callback(uploadResult, result);
|
||||
}
|
||||
results.add(SingleResponse.getInstance(result));
|
||||
} catch (final TwitterException e) {
|
||||
Log.w(LOGTAG, e);
|
||||
@ -869,13 +889,6 @@ public class BackgroundOperationService extends IntentService implements Constan
|
||||
}
|
||||
}
|
||||
|
||||
static class StatusTooLongException extends UpdateStatusException {
|
||||
|
||||
public StatusTooLongException(final Context context) {
|
||||
super(context.getString(R.string.error_message_status_too_long));
|
||||
}
|
||||
}
|
||||
|
||||
static class UpdateStatusException extends Exception {
|
||||
public UpdateStatusException() {
|
||||
super();
|
||||
|
@ -30,6 +30,8 @@ import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mariotaku.twidere.Constants;
|
||||
import org.mariotaku.twidere.model.MediaUploadResult;
|
||||
import org.mariotaku.twidere.model.ParcelableStatus;
|
||||
import org.mariotaku.twidere.util.ServiceUtils.ServiceToken;
|
||||
|
||||
import static org.mariotaku.twidere.util.ServiceUtils.bindToService;
|
||||
|
@ -30,18 +30,57 @@ import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mariotaku.twidere.BuildConfig;
|
||||
import org.mariotaku.twidere.IMediaUploader;
|
||||
import org.mariotaku.twidere.model.MediaUploadResult;
|
||||
import org.mariotaku.twidere.model.ParcelableStatus;
|
||||
import org.mariotaku.twidere.model.ParcelableStatusUpdate;
|
||||
import org.mariotaku.twidere.model.UploaderMediaItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class MediaUploaderInterface extends AbsServiceInterface<IMediaUploader> implements IMediaUploader {
|
||||
public final class MediaUploaderInterface extends AbsServiceInterface<IMediaUploader> {
|
||||
protected MediaUploaderInterface(Context context, String uploaderName, Bundle metaData) {
|
||||
super(context, uploaderName, metaData);
|
||||
}
|
||||
|
||||
public MediaUploadResult upload(final ParcelableStatusUpdate status, final UploaderMediaItem[] media)
|
||||
throws RemoteException {
|
||||
final IMediaUploader iface = getInterface();
|
||||
if (iface == null) return null;
|
||||
try {
|
||||
final String statusJson = JsonSerializer.serialize(status, ParcelableStatusUpdate.class);
|
||||
final String mediaJson = JsonSerializer.serialize(media, UploaderMediaItem.class);
|
||||
return JsonSerializer.parse(iface.upload(statusJson, mediaJson), MediaUploadResult.class);
|
||||
} catch (final RemoteException e) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOGTAG, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public boolean callback(MediaUploadResult uploadResult, ParcelableStatus status) {
|
||||
final IMediaUploader iface = getInterface();
|
||||
if (iface == null) return false;
|
||||
try {
|
||||
final String resultJson = JsonSerializer.serialize(uploadResult, MediaUploadResult.class);
|
||||
final String statusJson = JsonSerializer.serialize(status, ParcelableStatus.class);
|
||||
return iface.callback(resultJson, statusJson);
|
||||
} catch (final RemoteException e) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOGTAG, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IMediaUploader onServiceConnected(ComponentName service, IBinder obj) {
|
||||
return IMediaUploader.Stub.asInterface(obj);
|
||||
}
|
||||
|
||||
public static MediaUploaderInterface getInstance(final Application application, final String uploaderName) {
|
||||
if (uploaderName == null) return null;
|
||||
final Intent intent = new Intent(INTENT_ACTION_EXTENSION_UPLOAD_MEDIA);
|
||||
@ -52,22 +91,4 @@ public final class MediaUploaderInterface extends AbsServiceInterface<IMediaUplo
|
||||
if (services.size() != 1) return null;
|
||||
return new MediaUploaderInterface(application, uploaderName, services.get(0).serviceInfo.metaData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaUploadResult upload(final ParcelableStatusUpdate status, final UploaderMediaItem[] media)
|
||||
throws RemoteException {
|
||||
final IMediaUploader iface = getInterface();
|
||||
if (iface == null) return null;
|
||||
try {
|
||||
return iface.upload(status, media);
|
||||
} catch (final RemoteException e) {
|
||||
Log.w(LOGTAG, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IMediaUploader onServiceConnected(ComponentName service, IBinder obj) {
|
||||
return IMediaUploader.Stub.asInterface(obj);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,9 @@ import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mariotaku.twidere.BuildConfig;
|
||||
import org.mariotaku.twidere.IStatusShortener;
|
||||
import org.mariotaku.twidere.model.ParcelableStatus;
|
||||
import org.mariotaku.twidere.model.ParcelableStatusUpdate;
|
||||
@ -57,8 +59,11 @@ public final class StatusShortenerInterface extends AbsServiceInterface<IStatusS
|
||||
final String resultJson = iface.shorten(statusJson, currentAccountId, overrideStatusText);
|
||||
return JsonSerializer.parse(resultJson, StatusShortenResult.class);
|
||||
} catch (final RemoteException e) {
|
||||
return null;
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOGTAG, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean callback(StatusShortenResult result, ParcelableStatus status) {
|
||||
@ -69,8 +74,11 @@ public final class StatusShortenerInterface extends AbsServiceInterface<IStatusS
|
||||
final String statusJson = JsonSerializer.serialize(status, ParcelableStatus.class);
|
||||
return iface.callback(resultJson, statusJson);
|
||||
} catch (final RemoteException e) {
|
||||
return false;
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOGTAG, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static StatusShortenerInterface getInstance(final Application application, final String shortenerName) {
|
||||
|
@ -752,5 +752,6 @@
|
||||
<string name="status_menu_title_format"><xliff:g id="name">%1$s</xliff:g>: <xliff:g id="text">%2$s</xliff:g></string>
|
||||
<string name="translate_from_language">Translate from <xliff:g id="language">%s</xliff:g></string>
|
||||
<string name="translation">Translation</string>
|
||||
<string name="shortener_version_incompatible">Incompatible shortener</string>
|
||||
<string name="shortener_version_incompatible">Incompatible tweet shortener</string>
|
||||
<string name="uploader_version_incompatible">Incompatible media uploader</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user