fixed save some images in fanfou
This commit is contained in:
parent
3a9c3cef8d
commit
f2993fc488
|
@ -0,0 +1,18 @@
|
|||
package org.mariotaku.twidere.annotation;
|
||||
|
||||
import android.support.annotation.StringDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/2/4.
|
||||
*/
|
||||
|
||||
@StringDef({CacheFileType.IMAGE, CacheFileType.VIDEO, CacheFileType.JSON})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface CacheFileType {
|
||||
String IMAGE = "image";
|
||||
String VIDEO = "video";
|
||||
String JSON = "json";
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
package org.mariotaku.twidere.provider;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringDef;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.bluelinelabs.logansquare.JsonMapper;
|
||||
import com.nostra13.universalimageloader.cache.disc.DiskCache;
|
||||
|
||||
import org.mariotaku.commons.logansquare.LoganSquareMapperFinder;
|
||||
import org.mariotaku.restfu.RestFuUtils;
|
||||
import org.mariotaku.twidere.TwidereConstants;
|
||||
import org.mariotaku.twidere.model.CacheMetadata;
|
||||
import org.mariotaku.twidere.task.SaveFileTask;
|
||||
import org.mariotaku.twidere.util.BitmapUtils;
|
||||
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 16/1/1.
|
||||
*/
|
||||
public class CacheProvider extends ContentProvider implements TwidereConstants {
|
||||
@Inject
|
||||
DiskCache mSimpleDiskCache;
|
||||
|
||||
public static Uri getCacheUri(String key, @Type String type) {
|
||||
final Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme(ContentResolver.SCHEME_CONTENT);
|
||||
builder.authority(TwidereConstants.AUTHORITY_TWIDERE_CACHE);
|
||||
builder.appendPath(ByteString.encodeUtf8(key).base64Url());
|
||||
if (type != null) {
|
||||
builder.appendQueryParameter(QUERY_PARAM_TYPE, type);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getCacheKey(Uri uri) {
|
||||
if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()))
|
||||
throw new IllegalArgumentException(uri.toString());
|
||||
if (!TwidereConstants.AUTHORITY_TWIDERE_CACHE.equals(uri.getAuthority()))
|
||||
throw new IllegalArgumentException(uri.toString());
|
||||
return ByteString.decodeBase64(uri.getLastPathSegment()).utf8();
|
||||
}
|
||||
|
||||
|
||||
public static String getMetadataKey(Uri uri) {
|
||||
if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()))
|
||||
throw new IllegalArgumentException(uri.toString());
|
||||
if (!TwidereConstants.AUTHORITY_TWIDERE_CACHE.equals(uri.getAuthority()))
|
||||
throw new IllegalArgumentException(uri.toString());
|
||||
return getExtraKey(ByteString.decodeBase64(uri.getLastPathSegment()).utf8());
|
||||
}
|
||||
|
||||
public static String getExtraKey(String key) {
|
||||
return key + ".extra";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
final Context context = getContext();
|
||||
assert context != null;
|
||||
GeneralComponentHelper.build(context).inject(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
final CacheMetadata metadata = getMetadata(uri);
|
||||
if (metadata != null) {
|
||||
return metadata.getContentType();
|
||||
}
|
||||
final String type = uri.getQueryParameter(QUERY_PARAM_TYPE);
|
||||
if (type != null) {
|
||||
switch (type) {
|
||||
case Type.IMAGE: {
|
||||
final File file = mSimpleDiskCache.get(getCacheKey(uri));
|
||||
if (file == null) return null;
|
||||
return BitmapUtils.getImageMimeType(file);
|
||||
}
|
||||
case Type.VIDEO: {
|
||||
return "video/mp4";
|
||||
}
|
||||
case Type.JSON: {
|
||||
return "application/json";
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public CacheMetadata getMetadata(@NonNull Uri uri) {
|
||||
final File file = mSimpleDiskCache.get(getMetadataKey(uri));
|
||||
if (file == null) return null;
|
||||
FileInputStream is = null;
|
||||
try {
|
||||
final JsonMapper<CacheMetadata> mapper = LoganSquareMapperFinder.mapperFor(CacheMetadata.class);
|
||||
is = new FileInputStream(file);
|
||||
return mapper.parse(is);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
RestFuUtils.closeSilently(is);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
try {
|
||||
final File file = mSimpleDiskCache.get(getCacheKey(uri));
|
||||
if (file == null) throw new FileNotFoundException();
|
||||
final int modeBits = modeToMode(mode);
|
||||
if (modeBits != ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
throw new IllegalArgumentException("Cache can't be opened for write");
|
||||
return ParcelFileDescriptor.open(file, modeBits);
|
||||
} catch (IOException e) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from ContentResolver.java
|
||||
*/
|
||||
private static int modeToMode(String mode) {
|
||||
int modeBits;
|
||||
if ("r".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||
| ParcelFileDescriptor.MODE_CREATE
|
||||
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
} else if ("wa".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||
| ParcelFileDescriptor.MODE_CREATE
|
||||
| ParcelFileDescriptor.MODE_APPEND;
|
||||
} else if ("rw".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||
| ParcelFileDescriptor.MODE_CREATE;
|
||||
} else if ("rwt".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||
| ParcelFileDescriptor.MODE_CREATE
|
||||
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid mode: " + mode);
|
||||
}
|
||||
return modeBits;
|
||||
}
|
||||
|
||||
public static final class CacheFileTypeCallback implements SaveFileTask.FileInfoCallback {
|
||||
private final Context context;
|
||||
private final String type;
|
||||
|
||||
public CacheFileTypeCallback(Context context, @Type String type) {
|
||||
this.context = context;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String getFilename(@NonNull Uri source) {
|
||||
String cacheKey = getCacheKey(source);
|
||||
final int indexOfSsp = cacheKey.indexOf("://");
|
||||
if (indexOfSsp != -1) {
|
||||
cacheKey = cacheKey.substring(indexOfSsp + 3);
|
||||
}
|
||||
return cacheKey.replaceAll("[^\\w\\d_]", String.valueOf(getSpecialCharacter()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getMimeType(@NonNull Uri source) {
|
||||
if (type == null || source.getQueryParameter(QUERY_PARAM_TYPE) != null) {
|
||||
return context.getContentResolver().getType(source);
|
||||
}
|
||||
final Uri.Builder builder = source.buildUpon();
|
||||
builder.appendQueryParameter(QUERY_PARAM_TYPE, type);
|
||||
return context.getContentResolver().getType(builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension(@Nullable String mimeType) {
|
||||
if (mimeType == null) return null;
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType.toLowerCase(Locale.US));
|
||||
}
|
||||
|
||||
@Override
|
||||
public char getSpecialCharacter() {
|
||||
return '_';
|
||||
}
|
||||
}
|
||||
|
||||
@StringDef({Type.IMAGE, Type.VIDEO, Type.JSON})
|
||||
public @interface Type {
|
||||
String IMAGE = "image";
|
||||
String VIDEO = "video";
|
||||
String JSON = "json";
|
||||
}
|
||||
}
|
|
@ -293,6 +293,15 @@ public final class Utils implements Constants {
|
|||
}
|
||||
}
|
||||
|
||||
public static String sanitizeMimeType(@Nullable final String contentType) {
|
||||
if (contentType == null) return null;
|
||||
switch (contentType) {
|
||||
case "image/jpg":
|
||||
return "image/jpeg";
|
||||
}
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public static class NoAccountException extends Exception {
|
||||
String accountHost;
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.mariotaku.twidere.util.JsonSerializer;
|
|||
import org.mariotaku.twidere.util.MicroBlogAPIFactory;
|
||||
import org.mariotaku.twidere.util.SharedPreferencesWrapper;
|
||||
import org.mariotaku.twidere.util.UserAgentUtils;
|
||||
import org.mariotaku.twidere.util.Utils;
|
||||
import org.mariotaku.twidere.util.media.preview.PreviewMediaExtractor;
|
||||
import org.mariotaku.twidere.util.net.NoIntercept;
|
||||
|
||||
|
@ -176,7 +177,7 @@ public class TwidereMediaDownloader implements MediaDownloader, Constants {
|
|||
}
|
||||
final Body body = resp.getBody();
|
||||
final CacheMetadata metadata = new CacheMetadata();
|
||||
metadata.setContentType(body.contentType().getContentType());
|
||||
metadata.setContentType(Utils.sanitizeMimeType(body.contentType().getContentType()));
|
||||
return new TwidereDownloadResult(body, metadata);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,19 +45,19 @@ public class UILFileCache implements FileCache {
|
|||
}
|
||||
});
|
||||
if (extra != null) {
|
||||
cache.save(CacheProvider.getExtraKey(key), new ByteArrayInputStream(extra), null);
|
||||
cache.save(CacheProvider.Companion.getExtraKey(key), new ByteArrayInputStream(extra), null);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Uri toUri(@NonNull final String key) {
|
||||
return CacheProvider.getCacheUri(key, null);
|
||||
return CacheProvider.Companion.getCacheUri(key, null);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String fromUri(@NonNull final Uri uri) {
|
||||
return CacheProvider.getCacheKey(uri);
|
||||
return CacheProvider.Companion.getCacheKey(uri);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ import org.mariotaku.twidere.R
|
|||
import org.mariotaku.twidere.TwidereConstants.*
|
||||
import org.mariotaku.twidere.activity.iface.IControlBarActivity.ControlBarShowHideHelper
|
||||
import org.mariotaku.twidere.activity.iface.IExtendedActivity
|
||||
import org.mariotaku.twidere.annotation.CacheFileType
|
||||
import org.mariotaku.twidere.fragment.*
|
||||
import org.mariotaku.twidere.fragment.iface.IBaseFragment
|
||||
import org.mariotaku.twidere.model.ParcelableMedia
|
||||
|
@ -428,9 +429,9 @@ class MediaViewerActivity : BaseActivity(), IMediaViewerActivity, MediaSwipeClos
|
|||
val destination = ShareProvider.getFilesDir(this) ?: return
|
||||
val type: String
|
||||
when (f) {
|
||||
is VideoPageFragment -> type = CacheProvider.Type.VIDEO
|
||||
is ImagePageFragment -> type = CacheProvider.Type.IMAGE
|
||||
is GifPageFragment -> type = CacheProvider.Type.IMAGE
|
||||
is VideoPageFragment -> type = CacheFileType.VIDEO
|
||||
is ImagePageFragment -> type = CacheFileType.IMAGE
|
||||
is GifPageFragment -> type = CacheFileType.IMAGE
|
||||
else -> throw UnsupportedOperationException("Unsupported fragment $f")
|
||||
}
|
||||
val task = object : SaveFileTask(this@MediaViewerActivity, cacheUri, destination,
|
||||
|
@ -488,9 +489,9 @@ class MediaViewerActivity : BaseActivity(), IMediaViewerActivity, MediaSwipeClos
|
|||
val f = adapter.instantiateItem(viewPager, saveToStoragePosition) as? CacheDownloadMediaViewerFragment ?: return
|
||||
val cacheUri = f.downloadResult?.cacheUri ?: return
|
||||
val task: SaveFileTask = when (f) {
|
||||
is ImagePageFragment -> SaveMediaToGalleryTask.create(this, cacheUri, CacheProvider.Type.IMAGE)
|
||||
is VideoPageFragment -> SaveMediaToGalleryTask.create(this, cacheUri, CacheProvider.Type.VIDEO)
|
||||
is GifPageFragment -> SaveMediaToGalleryTask.create(this, cacheUri, CacheProvider.Type.IMAGE)
|
||||
is ImagePageFragment -> SaveMediaToGalleryTask.create(this, cacheUri, CacheFileType.IMAGE)
|
||||
is VideoPageFragment -> SaveMediaToGalleryTask.create(this, cacheUri, CacheFileType.VIDEO)
|
||||
is GifPageFragment -> SaveMediaToGalleryTask.create(this, cacheUri, CacheFileType.IMAGE)
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
AsyncTaskUtils.executeTask(task)
|
||||
|
|
|
@ -93,16 +93,15 @@ class WebLinkHandlerActivity : Activity() {
|
|||
return Pair(Intent(Intent.ACTION_VIEW, builder.build()), true)
|
||||
}
|
||||
else -> {
|
||||
if (!ArrayUtils.contains(FANFOU_RESERVED_PATHS, pathSegments[0])) {
|
||||
if (pathSegments.size == 1) {
|
||||
val builder = Uri.Builder()
|
||||
builder.scheme(SCHEME_TWIDERE)
|
||||
builder.authority(AUTHORITY_USER)
|
||||
builder.appendQueryParameter(QUERY_PARAM_ACCOUNT_HOST, USER_TYPE_FANFOU_COM)
|
||||
val userKey = UserKey(pathSegments[0], USER_TYPE_FANFOU_COM)
|
||||
builder.appendQueryParameter(QUERY_PARAM_USER_KEY, userKey.toString())
|
||||
return Pair(Intent(Intent.ACTION_VIEW, builder.build()), true)
|
||||
}
|
||||
if (FANFOU_RESERVED_PATHS.contains(pathSegments[0])) return Pair(null, false)
|
||||
if (pathSegments.size == 1) {
|
||||
val builder = Uri.Builder()
|
||||
builder.scheme(SCHEME_TWIDERE)
|
||||
builder.authority(AUTHORITY_USER)
|
||||
builder.appendQueryParameter(QUERY_PARAM_ACCOUNT_HOST, USER_TYPE_FANFOU_COM)
|
||||
val userKey = UserKey(pathSegments[0], USER_TYPE_FANFOU_COM)
|
||||
builder.appendQueryParameter(QUERY_PARAM_USER_KEY, userKey.toString())
|
||||
return Pair(Intent(Intent.ACTION_VIEW, builder.build()), true)
|
||||
}
|
||||
return Pair(null, false)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import android.support.v4.content.AsyncTaskLoader
|
|||
import org.mariotaku.ktextension.set
|
||||
import org.mariotaku.microblog.library.MicroBlogException
|
||||
import org.mariotaku.microblog.library.twitter.model.ErrorInfo
|
||||
import org.mariotaku.restfu.http.RestHttpClient
|
||||
import org.mariotaku.twidere.constant.IntentConstants
|
||||
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT
|
||||
import org.mariotaku.twidere.model.ParcelableStatus
|
||||
|
@ -51,7 +52,9 @@ class ParcelableStatusLoader(
|
|||
) : AsyncTaskLoader<SingleResponse<ParcelableStatus>>(context) {
|
||||
|
||||
@Inject
|
||||
lateinit var userColorNameManager: UserColorNameManager
|
||||
internal lateinit var userColorNameManager: UserColorNameManager
|
||||
@Inject
|
||||
internal lateinit var restHttpClient: RestHttpClient
|
||||
|
||||
init {
|
||||
GeneralComponentHelper.build(context).inject(this)
|
||||
|
@ -81,8 +84,7 @@ class ParcelableStatusLoader(
|
|||
if (e.errorCode == ErrorInfo.STATUS_NOT_FOUND) {
|
||||
// Delete all deleted status
|
||||
val cr = context.contentResolver
|
||||
DataStoreUtils.deleteStatus(cr, accountKey,
|
||||
statusId, null)
|
||||
DataStoreUtils.deleteStatus(cr, accountKey, statusId, null)
|
||||
DataStoreUtils.deleteActivityStatus(cr, accountKey, statusId, null)
|
||||
}
|
||||
return SingleResponse(e)
|
||||
|
@ -94,4 +96,5 @@ class ParcelableStatusLoader(
|
|||
forceLoad()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
package org.mariotaku.twidere.provider
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.nostra13.universalimageloader.cache.disc.DiskCache
|
||||
import okio.ByteString
|
||||
import org.mariotaku.commons.logansquare.LoganSquareMapperFinder
|
||||
import org.mariotaku.restfu.RestFuUtils
|
||||
import org.mariotaku.twidere.TwidereConstants
|
||||
import org.mariotaku.twidere.annotation.CacheFileType
|
||||
import org.mariotaku.twidere.model.CacheMetadata
|
||||
import org.mariotaku.twidere.task.SaveFileTask
|
||||
import org.mariotaku.twidere.util.BitmapUtils
|
||||
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 16/1/1.
|
||||
*/
|
||||
class CacheProvider : ContentProvider() {
|
||||
@Inject
|
||||
internal lateinit var simpleDiskCache: DiskCache
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
GeneralComponentHelper.build(context).inject(this)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
|
||||
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String? {
|
||||
val metadata = getMetadata(uri)
|
||||
if (metadata != null) {
|
||||
return metadata.contentType
|
||||
}
|
||||
val type = uri.getQueryParameter(TwidereConstants.QUERY_PARAM_TYPE)
|
||||
when (type) {
|
||||
CacheFileType.IMAGE -> {
|
||||
val file = simpleDiskCache.get(getCacheKey(uri)) ?: return null
|
||||
return BitmapUtils.getImageMimeType(file)
|
||||
}
|
||||
CacheFileType.VIDEO -> {
|
||||
return "video/mp4"
|
||||
}
|
||||
CacheFileType.JSON -> {
|
||||
return "application/json"
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getMetadata(uri: Uri): CacheMetadata? {
|
||||
val file = simpleDiskCache.get(getMetadataKey(uri)) ?: return null
|
||||
var `is`: FileInputStream? = null
|
||||
try {
|
||||
val mapper = LoganSquareMapperFinder.mapperFor(CacheMetadata::class.java)
|
||||
`is` = FileInputStream(file)
|
||||
return mapper.parse(`is`)
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
} finally {
|
||||
RestFuUtils.closeSilently(`is`)
|
||||
}
|
||||
}
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
try {
|
||||
val file = simpleDiskCache.get(getCacheKey(uri)) ?: throw FileNotFoundException()
|
||||
val modeBits = modeToMode(mode)
|
||||
if (modeBits != ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
throw IllegalArgumentException("Cache can't be opened for write")
|
||||
return ParcelFileDescriptor.open(file, modeBits)
|
||||
} catch (e: IOException) {
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CacheFileTypeCallback(private val context: Context, @CacheFileType private val type: String?) : SaveFileTask.FileInfoCallback {
|
||||
|
||||
override fun getFilename(source: Uri): String {
|
||||
var cacheKey = getCacheKey(source)
|
||||
val indexOfSsp = cacheKey.indexOf("://")
|
||||
if (indexOfSsp != -1) {
|
||||
cacheKey = cacheKey.substring(indexOfSsp + 3)
|
||||
}
|
||||
return cacheKey.replace("[^\\w\\d_]".toRegex(), specialCharacter.toString())
|
||||
}
|
||||
|
||||
override fun getMimeType(source: Uri): String? {
|
||||
if (type == null || source.getQueryParameter(TwidereConstants.QUERY_PARAM_TYPE) != null) {
|
||||
return context.contentResolver.getType(source)
|
||||
}
|
||||
val builder = source.buildUpon()
|
||||
builder.appendQueryParameter(TwidereConstants.QUERY_PARAM_TYPE, type)
|
||||
return context.contentResolver.getType(builder.build())
|
||||
}
|
||||
|
||||
override fun getExtension(mimeType: String?): String? {
|
||||
val typeLowered = mimeType?.toLowerCase(Locale.US) ?: return null
|
||||
return when (typeLowered) {
|
||||
// Hack for fanfou image type
|
||||
"image/jpg" -> "jpg"
|
||||
else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(typeLowered)
|
||||
}
|
||||
}
|
||||
|
||||
override val specialCharacter: Char
|
||||
get() = '_'
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
fun getCacheUri(key: String, @CacheFileType type: String?): Uri {
|
||||
val builder = Uri.Builder()
|
||||
builder.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
builder.authority(TwidereConstants.AUTHORITY_TWIDERE_CACHE)
|
||||
builder.appendPath(ByteString.encodeUtf8(key).base64Url())
|
||||
if (type != null) {
|
||||
builder.appendQueryParameter(TwidereConstants.QUERY_PARAM_TYPE, type)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun getCacheKey(uri: Uri): String {
|
||||
if (ContentResolver.SCHEME_CONTENT != uri.scheme)
|
||||
throw IllegalArgumentException(uri.toString())
|
||||
if (TwidereConstants.AUTHORITY_TWIDERE_CACHE != uri.authority)
|
||||
throw IllegalArgumentException(uri.toString())
|
||||
return ByteString.decodeBase64(uri.lastPathSegment).utf8()
|
||||
}
|
||||
|
||||
|
||||
fun getMetadataKey(uri: Uri): String {
|
||||
if (ContentResolver.SCHEME_CONTENT != uri.scheme)
|
||||
throw IllegalArgumentException(uri.toString())
|
||||
if (TwidereConstants.AUTHORITY_TWIDERE_CACHE != uri.authority)
|
||||
throw IllegalArgumentException(uri.toString())
|
||||
return getExtraKey(ByteString.decodeBase64(uri.lastPathSegment).utf8())
|
||||
}
|
||||
|
||||
fun getExtraKey(key: String): String {
|
||||
return key + ".extra"
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from ContentResolver.java
|
||||
*/
|
||||
private fun modeToMode(mode: String): Int {
|
||||
val modeBits: Int
|
||||
if ("r" == mode) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_ONLY
|
||||
} else if ("w" == mode || "wt" == mode) {
|
||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY or ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_TRUNCATE
|
||||
} else if ("wa" == mode) {
|
||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY or ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_APPEND
|
||||
} else if ("rw" == mode) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
|
||||
} else if ("rwt" == mode) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_TRUNCATE
|
||||
} else {
|
||||
throw IllegalArgumentException("Invalid mode: " + mode)
|
||||
}
|
||||
return modeBits
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,8 +33,12 @@ import java.io.File
|
|||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
abstract class SaveFileTask(context: Context, private val source: Uri,
|
||||
private val destination: File, private val getMimeType: SaveFileTask.FileInfoCallback) : AsyncTask<Any, Any, SaveFileTask.SaveFileResult>() {
|
||||
abstract class SaveFileTask(
|
||||
context: Context,
|
||||
private val source: Uri,
|
||||
private val destination: File,
|
||||
private val getMimeType: SaveFileTask.FileInfoCallback
|
||||
) : AsyncTask<Any, Any, SaveFileTask.SaveFileResult>() {
|
||||
|
||||
private val contextRef: WeakReference<Context>
|
||||
|
||||
|
@ -44,7 +48,7 @@ abstract class SaveFileTask(context: Context, private val source: Uri,
|
|||
|
||||
override fun doInBackground(vararg args: Any): SaveFileResult? {
|
||||
val context = contextRef.get() ?: return null
|
||||
return saveFile(context, source, getMimeType, destination)
|
||||
return saveFile(context, source, getMimeType, destination, requiresValidExtension)
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
|
@ -72,6 +76,7 @@ abstract class SaveFileTask(context: Context, private val source: Uri,
|
|||
|
||||
protected abstract fun dismissProgress()
|
||||
|
||||
open val requiresValidExtension: Boolean = false
|
||||
|
||||
protected val context: Context?
|
||||
get() = contextRef.get()
|
||||
|
@ -102,7 +107,7 @@ abstract class SaveFileTask(context: Context, private val source: Uri,
|
|||
|
||||
fun saveFile(context: Context, source: Uri,
|
||||
fileInfoCallback: FileInfoCallback,
|
||||
destinationDir: File): SaveFileResult? {
|
||||
destinationDir: File, requiresValidExtension: Boolean): SaveFileResult? {
|
||||
val cr = context.contentResolver
|
||||
var ioSrc: Source? = null
|
||||
var sink: BufferedSink? = null
|
||||
|
@ -114,6 +119,9 @@ abstract class SaveFileTask(context: Context, private val source: Uri,
|
|||
}
|
||||
val mimeType = fileInfoCallback.getMimeType(source) ?: return null
|
||||
val extension = fileInfoCallback.getExtension(mimeType)
|
||||
if (requiresValidExtension && extension == null) {
|
||||
return null
|
||||
}
|
||||
if (!destinationDir.isDirectory && !destinationDir.mkdirs()) return null
|
||||
var nameToSave = getFileNameWithExtension(name, extension,
|
||||
fileInfoCallback.specialCharacter, null)
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.net.Uri
|
|||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.annotation.CacheFileType
|
||||
import org.mariotaku.twidere.provider.CacheProvider
|
||||
import java.io.File
|
||||
|
||||
|
@ -52,14 +53,13 @@ class SaveMediaToGalleryTask(
|
|||
|
||||
companion object {
|
||||
|
||||
fun create(activity: Activity, source: Uri,
|
||||
@CacheProvider.Type type: String): SaveFileTask {
|
||||
fun create(activity: Activity, source: Uri, @CacheFileType type: String): SaveFileTask {
|
||||
val pubDir: File
|
||||
when (type) {
|
||||
CacheProvider.Type.VIDEO -> {
|
||||
CacheFileType.VIDEO -> {
|
||||
pubDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
|
||||
}
|
||||
CacheProvider.Type.IMAGE -> {
|
||||
CacheFileType.IMAGE -> {
|
||||
pubDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
}
|
||||
else -> {
|
||||
|
|
|
@ -42,6 +42,7 @@ import org.mariotaku.twidere.constant.newDocumentApiKey
|
|||
import org.mariotaku.twidere.model.UserKey
|
||||
import org.mariotaku.twidere.model.util.ParcelableMediaUtils
|
||||
import org.mariotaku.twidere.util.TwidereLinkify.OnLinkClickListener
|
||||
import org.mariotaku.twidere.util.TwidereLinkify.USER_TYPE_FANFOU_COM
|
||||
import org.mariotaku.twidere.util.media.preview.PreviewMediaExtractor
|
||||
|
||||
open class OnLinkClickHandler(
|
||||
|
@ -72,16 +73,16 @@ open class OnLinkClickHandler(
|
|||
return true
|
||||
}
|
||||
TwidereLinkify.LINK_TYPE_LINK_IN_TEXT -> {
|
||||
if (isMedia(link, extraId)) {
|
||||
openMedia(accountKey!!, extraId, sensitive, link, start, end)
|
||||
if (accountKey != null && isMedia(link, extraId)) {
|
||||
openMedia(accountKey, extraId, sensitive, link, start, end)
|
||||
} else {
|
||||
openLink(link)
|
||||
}
|
||||
return true
|
||||
}
|
||||
TwidereLinkify.LINK_TYPE_ENTITY_URL -> {
|
||||
if (isMedia(link, extraId)) {
|
||||
openMedia(accountKey!!, extraId, sensitive, link, start, end)
|
||||
if (accountKey != null && isMedia(link, extraId)) {
|
||||
openMedia(accountKey, extraId, sensitive, link, start, end)
|
||||
} else {
|
||||
val authority = UriUtils.getAuthority(link)
|
||||
if (authority == null) {
|
||||
|
@ -89,36 +90,12 @@ open class OnLinkClickHandler(
|
|||
return true
|
||||
}
|
||||
when (authority) {
|
||||
"fanfou.com" -> {
|
||||
if (orig != null) {
|
||||
// Process special case for fanfou
|
||||
val ch = orig[0]
|
||||
// Extend selection
|
||||
val length = orig.length
|
||||
if (TwidereLinkify.isAtSymbol(ch)) {
|
||||
var id = UriUtils.getPath(link)
|
||||
if (id != null) {
|
||||
val idxOfSlash = id.indexOf('/')
|
||||
if (idxOfSlash == 0) {
|
||||
id = id.substring(1)
|
||||
}
|
||||
val screenName = orig.substring(1, length)
|
||||
IntentUtils.openUserProfile(context, accountKey, UserKey.valueOf(id),
|
||||
screenName, preferences[newDocumentApiKey], Referral.USER_MENTION,
|
||||
null)
|
||||
return true
|
||||
}
|
||||
} else if (TwidereLinkify.isHashSymbol(ch) && TwidereLinkify.isHashSymbol(orig[length - 1])) {
|
||||
IntentUtils.openSearch(context, accountKey, orig.substring(1, length - 1))
|
||||
return true
|
||||
}
|
||||
}
|
||||
"fanfou.com" -> if (accountKey != null && handleFanfouLink(link, orig, accountKey)) {
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
if (IntentUtils.isWebLinkHandled(context, Uri.parse(link))) {
|
||||
openTwitterLink(link, accountKey!!)
|
||||
return true
|
||||
}
|
||||
else -> if (IntentUtils.isWebLinkHandled(context, Uri.parse(link))) {
|
||||
openTwitterLink(link, accountKey!!)
|
||||
return true
|
||||
}
|
||||
}
|
||||
openLink(link)
|
||||
|
@ -203,4 +180,36 @@ open class OnLinkClickHandler(
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFanfouLink(link: String, orig: String?, accountKey: UserKey): Boolean {
|
||||
if (orig == null) return false
|
||||
// Process special case for fanfou
|
||||
val ch = orig[0]
|
||||
// Extend selection
|
||||
val length = orig.length
|
||||
if (TwidereLinkify.isAtSymbol(ch)) {
|
||||
var id = UriUtils.getPath(link)
|
||||
if (id != null) {
|
||||
val idxOfSlash = id.indexOf('/')
|
||||
if (idxOfSlash == 0) {
|
||||
id = id.substring(1)
|
||||
}
|
||||
val screenName = orig.substring(1, length)
|
||||
IntentUtils.openUserProfile(context, accountKey, UserKey.valueOf(id),
|
||||
screenName, preferences[newDocumentApiKey], Referral.USER_MENTION,
|
||||
null)
|
||||
return true
|
||||
}
|
||||
} else if (TwidereLinkify.isHashSymbol(ch) && TwidereLinkify.isHashSymbol(orig[length - 1])) {
|
||||
IntentUtils.openSearch(context, accountKey, orig.substring(1, length - 1))
|
||||
return true
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
||||
intent.setClass(context, WebLinkHandlerActivity::class.java)
|
||||
if (accountKey.host == USER_TYPE_FANFOU_COM) {
|
||||
intent.putExtra(EXTRA_ACCOUNT_KEY, accountKey)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,9 +42,6 @@ class NameView(context: Context, attrs: AttributeSet? = null) : AppCompatTextVie
|
|||
|
||||
var nameFirst: Boolean = false
|
||||
var twoLine: Boolean = false
|
||||
get() {
|
||||
return maxLines >= 2
|
||||
}
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
|
|
|
@ -287,7 +287,7 @@
|
|||
<string name="cards">Cards</string>
|
||||
|
||||
<string name="clear_cache">Clear cache</string>
|
||||
<string name="clear_cache_summary">Clear stored profile image cache.</string>
|
||||
<string name="clear_cache_summary">Clear stored media cache.</string>
|
||||
<string name="clear_databases">Clear databases</string>
|
||||
<string name="clear_databases_summary">Clear all tweets, profiles, messages. Your account credentials won\'t be lost.</string>
|
||||
<string name="clear_search_history">Clear search history</string>
|
||||
|
|
Loading…
Reference in New Issue