using file provider to share files
changed all classes ended with Trojan 'cause some anti-virus apps indicates this a virus, LMFAO
This commit is contained in:
parent
e1f1e5299b
commit
54a1bcdc9c
|
@ -24,7 +24,7 @@ import java.lang.reflect.Type;
|
||||||
/**
|
/**
|
||||||
* Created by mariotaku on 15/12/13.
|
* Created by mariotaku on 15/12/13.
|
||||||
*/
|
*/
|
||||||
public class ParameterizedTypeTrojan {
|
public class ParameterizedTypeAccessor {
|
||||||
|
|
||||||
public static <T> ParameterizedType<T> create(Type type) {
|
public static <T> ParameterizedType<T> create(Type type) {
|
||||||
return new ParameterizedType.ConcreteParameterizedType<>(type);
|
return new ParameterizedType.ConcreteParameterizedType<>(type);
|
|
@ -67,6 +67,8 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst
|
||||||
String PROTOCOL_CONTENT = SCHEME_CONTENT + "://";
|
String PROTOCOL_CONTENT = SCHEME_CONTENT + "://";
|
||||||
String PROTOCOL_TWIDERE = SCHEME_TWIDERE + "://";
|
String PROTOCOL_TWIDERE = SCHEME_TWIDERE + "://";
|
||||||
|
|
||||||
|
String AUTHORITY_TWIDERE_FILE = "twidere.file";
|
||||||
|
|
||||||
String AUTHORITY_USER = "user";
|
String AUTHORITY_USER = "user";
|
||||||
String AUTHORITY_HOME = "home";
|
String AUTHORITY_HOME = "home";
|
||||||
String AUTHORITY_MENTIONS = "mentions";
|
String AUTHORITY_MENTIONS = "mentions";
|
||||||
|
|
|
@ -23,7 +23,7 @@ import android.support.v4.util.SimpleArrayMap;
|
||||||
|
|
||||||
import com.bluelinelabs.logansquare.LoganSquare;
|
import com.bluelinelabs.logansquare.LoganSquare;
|
||||||
import com.bluelinelabs.logansquare.ParameterizedType;
|
import com.bluelinelabs.logansquare.ParameterizedType;
|
||||||
import com.bluelinelabs.logansquare.ParameterizedTypeTrojan;
|
import com.bluelinelabs.logansquare.ParameterizedTypeAccessor;
|
||||||
import com.fasterxml.jackson.core.JsonParseException;
|
import com.fasterxml.jackson.core.JsonParseException;
|
||||||
|
|
||||||
import org.mariotaku.restfu.Converter;
|
import org.mariotaku.restfu.Converter;
|
||||||
|
@ -69,7 +69,7 @@ public class TwitterConverter implements Converter {
|
||||||
|
|
||||||
private static <T> T parseOrThrow(RestHttpResponse resp, InputStream stream, Type type) throws IOException, TwitterException {
|
private static <T> T parseOrThrow(RestHttpResponse resp, InputStream stream, Type type) throws IOException, TwitterException {
|
||||||
try {
|
try {
|
||||||
final ParameterizedType<T> parameterizedType = ParameterizedTypeTrojan.create(type);
|
final ParameterizedType<T> parameterizedType = ParameterizedTypeAccessor.create(type);
|
||||||
final T parse = LoganSquare.parse(stream, parameterizedType);
|
final T parse = LoganSquare.parse(stream, parameterizedType);
|
||||||
if (TwitterException.class == type && parse == null) {
|
if (TwitterException.class == type && parse == null) {
|
||||||
throw new TwitterException();
|
throw new TwitterException();
|
||||||
|
|
|
@ -114,6 +114,7 @@ dependencies {
|
||||||
compile fileTree(dir: 'libs/main', include: ['*.jar'])
|
compile fileTree(dir: 'libs/main', include: ['*.jar'])
|
||||||
provided 'javax.annotation:jsr250-api:1.0'
|
provided 'javax.annotation:jsr250-api:1.0'
|
||||||
// googleCompile fileTree(dir: 'libs/google', include: ['*.jar'])
|
// googleCompile fileTree(dir: 'libs/google', include: ['*.jar'])
|
||||||
|
compile 'com.google.android.gms:play-services-appindexing:8.1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
task svgToDrawable(type: SvgDrawableTask) {
|
task svgToDrawable(type: SvgDrawableTask) {
|
||||||
|
|
|
@ -526,6 +526,15 @@
|
||||||
android:name=".provider.RecentSearchProvider"
|
android:name=".provider.RecentSearchProvider"
|
||||||
android:authorities="org.mariotaku.twidere.provider.SearchRecentSuggestions"
|
android:authorities="org.mariotaku.twidere.provider.SearchRecentSuggestions"
|
||||||
tools:ignore="ExportedContentProvider"/>
|
tools:ignore="ExportedContentProvider"/>
|
||||||
|
<provider
|
||||||
|
android:name="android.support.v4.content.FileProvider"
|
||||||
|
android:authorities="twidere.file"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"/>
|
||||||
|
</provider>
|
||||||
|
|
||||||
<receiver android:name=".receiver.ConnectivityStateReceiver">
|
<receiver android:name=".receiver.ConnectivityStateReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -558,6 +567,11 @@
|
||||||
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
|
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<!-- ATTENTION: This was auto-generated to add Google Play services to your project for
|
||||||
|
App Indexing. See https://g.co/AppIndexing/AndroidStudio for more information. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.version"
|
||||||
|
android:value="@integer/google_play_services_version"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package android.support.v4.app;
|
package android.support.v4.app;
|
||||||
|
|
||||||
public class BackStackEntryTrojan {
|
public class BackStackEntryAccessor {
|
||||||
|
|
||||||
public static Fragment getFragmentInBackStackRecord(final FragmentManager.BackStackEntry entry) {
|
public static Fragment getFragmentInBackStackRecord(final FragmentManager.BackStackEntry entry) {
|
||||||
if (entry instanceof BackStackRecord) return ((BackStackRecord) entry).mHead.fragment;
|
if (entry instanceof BackStackRecord) return ((BackStackRecord) entry).mHead.fragment;
|
|
@ -2,7 +2,7 @@ package android.support.v4.app;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
public class FragmentTrojan {
|
public class FragmentAccessor {
|
||||||
|
|
||||||
public static Bundle getSavedFragmentState(final Fragment f) {
|
public static Bundle getSavedFragmentState(final Fragment f) {
|
||||||
return f.mSavedFragmentState;
|
return f.mSavedFragmentState;
|
|
@ -2,7 +2,7 @@ package android.support.v4.app;
|
||||||
|
|
||||||
import android.support.v4.view.LayoutInflaterFactory;
|
import android.support.v4.view.LayoutInflaterFactory;
|
||||||
|
|
||||||
public class FragmentManagerTrojan {
|
public class FragmentManagerAccessor {
|
||||||
|
|
||||||
public static boolean isStateSaved(final FragmentManager fm) {
|
public static boolean isStateSaved(final FragmentManager fm) {
|
||||||
if (fm instanceof FragmentManagerImpl) return ((FragmentManagerImpl) fm).mStateSaved;
|
if (fm instanceof FragmentManagerImpl) return ((FragmentManagerImpl) fm).mStateSaved;
|
|
@ -22,7 +22,7 @@ package android.support.v4.content;
|
||||||
/**
|
/**
|
||||||
* Created by mariotaku on 15/7/5.
|
* Created by mariotaku on 15/7/5.
|
||||||
*/
|
*/
|
||||||
public class LoaderTrojan {
|
public class LoaderAccessor {
|
||||||
public static <T> boolean isContentChanged(final Loader<T> loader) {
|
public static <T> boolean isContentChanged(final Loader<T> loader) {
|
||||||
return loader.mContentChanged;
|
return loader.mContentChanged;
|
||||||
}
|
}
|
|
@ -24,7 +24,7 @@ import android.view.View;
|
||||||
/**
|
/**
|
||||||
* Created by mariotaku on 15/7/18.
|
* Created by mariotaku on 15/7/18.
|
||||||
*/
|
*/
|
||||||
public class DrawerLayoutTrojan {
|
public class DrawerLayoutAccessor {
|
||||||
|
|
||||||
public static View findDrawerWithGravity(DrawerLayout layout, int gravity) {
|
public static View findDrawerWithGravity(DrawerLayout layout, int gravity) {
|
||||||
return layout.findDrawerWithGravity(gravity);
|
return layout.findDrawerWithGravity(gravity);
|
|
@ -24,7 +24,7 @@ import android.support.annotation.Nullable;
|
||||||
/**
|
/**
|
||||||
* Created by mariotaku on 15/4/27.
|
* Created by mariotaku on 15/4/27.
|
||||||
*/
|
*/
|
||||||
public class AppCompatDelegateTrojan {
|
public class AppCompatDelegateAccessor {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static ActionBar peekActionBar(@Nullable AppCompatDelegate delegate) {
|
public static ActionBar peekActionBar(@Nullable AppCompatDelegate delegate) {
|
|
@ -24,7 +24,7 @@ import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||||
/**
|
/**
|
||||||
* Created by mariotaku on 14/12/6.
|
* Created by mariotaku on 14/12/6.
|
||||||
*/
|
*/
|
||||||
public class ViewHolderTrojan {
|
public class ViewHolderAccessor {
|
||||||
|
|
||||||
public static boolean isRemoved(ViewHolder holder) {
|
public static boolean isRemoved(ViewHolder holder) {
|
||||||
return holder.isRemoved();
|
return holder.isRemoved();
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Twidere - Twitter client for Android
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.mariotaku.twidere.activity.iface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by mariotaku on 15/12/28.
|
||||||
|
*/
|
||||||
|
public interface IExtendedActivity {
|
||||||
|
|
||||||
|
void executeAfterFragmentResumed(Action action);
|
||||||
|
|
||||||
|
interface Action {
|
||||||
|
void execute(IExtendedActivity activity);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import com.squareup.otto.Bus;
|
||||||
|
|
||||||
import org.mariotaku.twidere.Constants;
|
import org.mariotaku.twidere.Constants;
|
||||||
import org.mariotaku.twidere.activity.iface.IControlBarActivity;
|
import org.mariotaku.twidere.activity.iface.IControlBarActivity;
|
||||||
|
import org.mariotaku.twidere.activity.iface.IExtendedActivity;
|
||||||
import org.mariotaku.twidere.app.TwidereApplication;
|
import org.mariotaku.twidere.app.TwidereApplication;
|
||||||
import org.mariotaku.twidere.fragment.iface.IBaseFragment.SystemWindowsInsetsCallback;
|
import org.mariotaku.twidere.fragment.iface.IBaseFragment.SystemWindowsInsetsCallback;
|
||||||
import org.mariotaku.twidere.util.ActivityTracker;
|
import org.mariotaku.twidere.util.ActivityTracker;
|
||||||
|
@ -46,13 +47,15 @@ import org.mariotaku.twidere.util.dagger.DaggerGeneralComponent;
|
||||||
import org.mariotaku.twidere.view.iface.IExtendedView.OnFitSystemWindowsListener;
|
import org.mariotaku.twidere.view.iface.IExtendedView.OnFitSystemWindowsListener;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@SuppressLint("Registered")
|
@SuppressLint("Registered")
|
||||||
public class BaseAppCompatActivity extends ThemedAppCompatActivity implements Constants,
|
public class BaseAppCompatActivity extends ThemedAppCompatActivity implements Constants,
|
||||||
OnFitSystemWindowsListener, SystemWindowsInsetsCallback, IControlBarActivity,
|
OnFitSystemWindowsListener, SystemWindowsInsetsCallback, IControlBarActivity,
|
||||||
KeyboardShortcutCallback {
|
KeyboardShortcutCallback, IExtendedActivity {
|
||||||
|
|
||||||
// Utility classes
|
// Utility classes
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -78,6 +81,8 @@ public class BaseAppCompatActivity extends ThemedAppCompatActivity implements Co
|
||||||
private boolean mIsVisible;
|
private boolean mIsVisible;
|
||||||
private Rect mSystemWindowsInsets;
|
private Rect mSystemWindowsInsets;
|
||||||
private int mKeyMetaState;
|
private int mKeyMetaState;
|
||||||
|
private boolean mFragmentResumed;
|
||||||
|
private Queue<Action> mActionQueue = new LinkedList<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean getSystemWindowsInsets(Rect insets) {
|
public boolean getSystemWindowsInsets(Rect insets) {
|
||||||
|
@ -183,6 +188,7 @@ public class BaseAppCompatActivity extends ThemedAppCompatActivity implements Co
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
|
mFragmentResumed = false;
|
||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,4 +256,24 @@ public class BaseAppCompatActivity extends ThemedAppCompatActivity implements Co
|
||||||
return mKeyMetaState;
|
return mKeyMetaState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResumeFragments() {
|
||||||
|
super.onResumeFragments();
|
||||||
|
mFragmentResumed = true;
|
||||||
|
executePending();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executeAfterFragmentResumed(Action action) {
|
||||||
|
mActionQueue.add(action);
|
||||||
|
executePending();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executePending() {
|
||||||
|
if (!mFragmentResumed) return;
|
||||||
|
Action action;
|
||||||
|
while ((action = mActionQueue.poll()) != null) {
|
||||||
|
action.execute(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v4.view.ViewPager;
|
import android.support.v4.view.ViewPager;
|
||||||
import android.support.v4.view.ViewPager.OnPageChangeListener;
|
import android.support.v4.view.ViewPager.OnPageChangeListener;
|
||||||
import android.support.v4.widget.DrawerLayout;
|
import android.support.v4.widget.DrawerLayout;
|
||||||
import android.support.v4.widget.DrawerLayoutTrojan;
|
import android.support.v4.widget.DrawerLayoutAccessor;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
@ -257,7 +257,7 @@ public class HomeActivity extends BaseAppCompatActivity implements OnClickListen
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ACTION_NAVIGATION_PREVIOUS_TAB: {
|
case ACTION_NAVIGATION_PREVIOUS_TAB: {
|
||||||
final int previous = mViewPager.getCurrentItem() - 1;
|
final int previous = mViewPager.getCurrentItem() - 1;
|
||||||
if (previous < 0 && DrawerLayoutTrojan.findDrawerWithGravity(mDrawerLayout, Gravity.START) != null) {
|
if (previous < 0 && DrawerLayoutAccessor.findDrawerWithGravity(mDrawerLayout, Gravity.START) != null) {
|
||||||
mDrawerLayout.openDrawer(GravityCompat.START);
|
mDrawerLayout.openDrawer(GravityCompat.START);
|
||||||
setControlBarVisibleAnimate(true);
|
setControlBarVisibleAnimate(true);
|
||||||
} else if (previous < mPagerAdapter.getCount()) {
|
} else if (previous < mPagerAdapter.getCount()) {
|
||||||
|
@ -271,7 +271,7 @@ public class HomeActivity extends BaseAppCompatActivity implements OnClickListen
|
||||||
}
|
}
|
||||||
case ACTION_NAVIGATION_NEXT_TAB: {
|
case ACTION_NAVIGATION_NEXT_TAB: {
|
||||||
final int next = mViewPager.getCurrentItem() + 1;
|
final int next = mViewPager.getCurrentItem() + 1;
|
||||||
if (next >= mPagerAdapter.getCount() && DrawerLayoutTrojan.findDrawerWithGravity(mDrawerLayout, Gravity.END) != null) {
|
if (next >= mPagerAdapter.getCount() && DrawerLayoutAccessor.findDrawerWithGravity(mDrawerLayout, Gravity.END) != null) {
|
||||||
mDrawerLayout.openDrawer(GravityCompat.END);
|
mDrawerLayout.openDrawer(GravityCompat.END);
|
||||||
setControlBarVisibleAnimate(true);
|
setControlBarVisibleAnimate(true);
|
||||||
} else if (next >= 0) {
|
} else if (next >= 0) {
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class LinkHandlerActivity extends BaseAppCompatActivity implements System
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getThemeResourceId() {
|
public int getThemeResourceId() {
|
||||||
return ThemeUtils.getDialogWhenLargeThemeResource(this);
|
return ThemeUtils.getNoActionBarThemeResource(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
package org.mariotaku.twidere.activity.support;
|
package org.mariotaku.twidere.activity.support;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.DialogFragment;
|
||||||
|
import android.app.FragmentManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.MediaPlayer;
|
import android.media.MediaPlayer;
|
||||||
|
@ -32,8 +35,10 @@ import android.os.Handler;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import android.support.v4.app.FragmentTransaction;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
|
import android.support.v4.content.FileProvider;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v4.util.Pair;
|
import android.support.v4.util.Pair;
|
||||||
import android.support.v4.view.ViewPager;
|
import android.support.v4.view.ViewPager;
|
||||||
|
@ -58,15 +63,15 @@ import android.widget.Toast;
|
||||||
|
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource;
|
import com.davemorrissey.labs.subscaleview.ImageSource;
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
|
||||||
import com.desmond.asyncmanager.AsyncManager;
|
|
||||||
import com.desmond.asyncmanager.TaskRunnable;
|
|
||||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||||
import com.sprylab.android.widget.TextureVideoView;
|
import com.sprylab.android.widget.TextureVideoView;
|
||||||
|
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.mariotaku.twidere.Constants;
|
import org.mariotaku.twidere.Constants;
|
||||||
import org.mariotaku.twidere.R;
|
import org.mariotaku.twidere.R;
|
||||||
|
import org.mariotaku.twidere.activity.iface.IExtendedActivity;
|
||||||
import org.mariotaku.twidere.adapter.support.SupportFixedFragmentStatePagerAdapter;
|
import org.mariotaku.twidere.adapter.support.SupportFixedFragmentStatePagerAdapter;
|
||||||
|
import org.mariotaku.twidere.fragment.ProgressDialogFragment;
|
||||||
import org.mariotaku.twidere.fragment.support.BaseSupportFragment;
|
import org.mariotaku.twidere.fragment.support.BaseSupportFragment;
|
||||||
import org.mariotaku.twidere.fragment.support.ViewStatusDialogFragment;
|
import org.mariotaku.twidere.fragment.support.ViewStatusDialogFragment;
|
||||||
import org.mariotaku.twidere.loader.support.TileImageLoader;
|
import org.mariotaku.twidere.loader.support.TileImageLoader;
|
||||||
|
@ -75,11 +80,13 @@ import org.mariotaku.twidere.loader.support.TileImageLoader.Result;
|
||||||
import org.mariotaku.twidere.model.ParcelableMedia;
|
import org.mariotaku.twidere.model.ParcelableMedia;
|
||||||
import org.mariotaku.twidere.model.ParcelableMedia.VideoInfo.Variant;
|
import org.mariotaku.twidere.model.ParcelableMedia.VideoInfo.Variant;
|
||||||
import org.mariotaku.twidere.model.ParcelableStatus;
|
import org.mariotaku.twidere.model.ParcelableStatus;
|
||||||
|
import org.mariotaku.twidere.task.ProgressSaveFileTask;
|
||||||
|
import org.mariotaku.twidere.task.SaveFileTask;
|
||||||
|
import org.mariotaku.twidere.task.SaveImageToGalleryTask;
|
||||||
import org.mariotaku.twidere.util.AsyncTaskUtils;
|
import org.mariotaku.twidere.util.AsyncTaskUtils;
|
||||||
import org.mariotaku.twidere.util.KeyboardShortcutsHandler;
|
import org.mariotaku.twidere.util.KeyboardShortcutsHandler;
|
||||||
import org.mariotaku.twidere.util.MenuUtils;
|
import org.mariotaku.twidere.util.MenuUtils;
|
||||||
import org.mariotaku.twidere.util.PermissionUtils;
|
import org.mariotaku.twidere.util.PermissionUtils;
|
||||||
import org.mariotaku.twidere.util.SaveFileTask;
|
|
||||||
import org.mariotaku.twidere.util.ThemeUtils;
|
import org.mariotaku.twidere.util.ThemeUtils;
|
||||||
import org.mariotaku.twidere.util.Utils;
|
import org.mariotaku.twidere.util.Utils;
|
||||||
import org.mariotaku.twidere.util.VideoLoader.VideoLoadingListener;
|
import org.mariotaku.twidere.util.VideoLoader.VideoLoadingListener;
|
||||||
|
@ -258,6 +265,8 @@ public final class MediaViewerActivity extends BaseAppCompatActivity implements
|
||||||
public static class BaseImagePageFragment extends AbsMediaPageFragment
|
public static class BaseImagePageFragment extends AbsMediaPageFragment
|
||||||
implements DownloadListener, LoaderCallbacks<Result>, OnClickListener {
|
implements DownloadListener, LoaderCallbacks<Result>, OnClickListener {
|
||||||
|
|
||||||
|
private static final int REQUEST_SHARE_IMAGE = 201;
|
||||||
|
|
||||||
private SubsamplingScaleImageView mImageView;
|
private SubsamplingScaleImageView mImageView;
|
||||||
private ProgressWheel mProgressBar;
|
private ProgressWheel mProgressBar;
|
||||||
private boolean mLoaderInitialized;
|
private boolean mLoaderInitialized;
|
||||||
|
@ -265,6 +274,7 @@ public final class MediaViewerActivity extends BaseAppCompatActivity implements
|
||||||
private SaveFileTask mSaveFileTask;
|
private SaveFileTask mSaveFileTask;
|
||||||
|
|
||||||
private File mImageFile;
|
private File mImageFile;
|
||||||
|
private File mShareImageFile;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBaseViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
public void onBaseViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
@ -394,7 +404,7 @@ public final class MediaViewerActivity extends BaseAppCompatActivity implements
|
||||||
final File file = mImageFile;
|
final File file = mImageFile;
|
||||||
final boolean hasImage = file != null && file.exists();
|
final boolean hasImage = file != null && file.exists();
|
||||||
if (!hasImage) return;
|
if (!hasImage) return;
|
||||||
mSaveFileTask = SaveFileTask.saveImage(getActivity(), file);
|
mSaveFileTask = SaveImageToGalleryTask.create(getActivity(), file);
|
||||||
AsyncTaskUtils.executeTask(mSaveFileTask);
|
AsyncTaskUtils.executeTask(mSaveFileTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,44 +412,10 @@ public final class MediaViewerActivity extends BaseAppCompatActivity implements
|
||||||
public void onPrepareOptionsMenu(Menu menu) {
|
public void onPrepareOptionsMenu(Menu menu) {
|
||||||
super.onPrepareOptionsMenu(menu);
|
super.onPrepareOptionsMenu(menu);
|
||||||
final boolean isLoading = getLoaderManager().hasRunningLoaders();
|
final boolean isLoading = getLoaderManager().hasRunningLoaders();
|
||||||
final TaskRunnable<File, Pair<Boolean, Intent>, Pair<Fragment, Menu>> checkState
|
final boolean hasImage = mImageFile != null;
|
||||||
= new TaskRunnable<File, Pair<Boolean, Intent>, Pair<Fragment, Menu>>() {
|
MenuUtils.setMenuItemAvailability(menu, R.id.refresh, !hasImage && !isLoading);
|
||||||
@Override
|
MenuUtils.setMenuItemAvailability(menu, R.id.share, hasImage && !isLoading);
|
||||||
public Pair<Boolean, Intent> doLongOperation(File file) throws InterruptedException {
|
MenuUtils.setMenuItemAvailability(menu, R.id.save, hasImage && !isLoading);
|
||||||
final boolean hasImage = file != null && file.exists();
|
|
||||||
if (!hasImage) {
|
|
||||||
return Pair.create(false, null);
|
|
||||||
}
|
|
||||||
final Intent intent = new Intent(Intent.ACTION_SEND);
|
|
||||||
final Uri fileUri = Uri.fromFile(file);
|
|
||||||
final String imageMimeType = Utils.getImageMimeType(file);
|
|
||||||
intent.setDataAndType(fileUri, imageMimeType);
|
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
|
|
||||||
final MediaViewerActivity activity = (MediaViewerActivity) getActivity();
|
|
||||||
if (activity.hasStatus()) {
|
|
||||||
final ParcelableStatus status = activity.getStatus();
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, Utils.getStatusShareText(activity, status));
|
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, Utils.getStatusShareSubject(activity, status));
|
|
||||||
}
|
|
||||||
return Pair.create(true, intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void callback(Pair<Fragment, Menu> callback, Pair<Boolean, Intent> result) {
|
|
||||||
if (callback.first.isDetached() || callback.first.getActivity() == null) return;
|
|
||||||
final Menu menu = callback.second;
|
|
||||||
final boolean hasImage = result.first;
|
|
||||||
MenuUtils.setMenuItemAvailability(menu, R.id.refresh, !hasImage && !isLoading);
|
|
||||||
MenuUtils.setMenuItemAvailability(menu, R.id.share, hasImage && !isLoading);
|
|
||||||
MenuUtils.setMenuItemAvailability(menu, R.id.save, hasImage && !isLoading);
|
|
||||||
if (!hasImage) return;
|
|
||||||
final MenuItem shareItem = menu.findItem(R.id.share);
|
|
||||||
shareItem.setIntent(Intent.createChooser(result.second, callback.first.getString(R.string.share)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
checkState.setParams(mImageFile);
|
|
||||||
checkState.setResultHandler(Pair.<Fragment, Menu>create(this, menu));
|
|
||||||
AsyncManager.runBackgroundTask(checkState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -464,6 +440,65 @@ public final class MediaViewerActivity extends BaseAppCompatActivity implements
|
||||||
loadImage();
|
loadImage();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case R.id.share: {
|
||||||
|
final FragmentActivity activity = getActivity();
|
||||||
|
final File destination = new File(activity.getCacheDir(), "shared_files");
|
||||||
|
final SaveFileTask task = new SaveFileTask(activity, mImageFile, destination,
|
||||||
|
new SaveImageToGalleryTask.ImageMimeTypeCallback()) {
|
||||||
|
private static final String PROGRESS_FRAGMENT_TAG = "progress";
|
||||||
|
|
||||||
|
protected void dismissProgress() {
|
||||||
|
final MediaViewerActivity activity = (MediaViewerActivity) getActivity();
|
||||||
|
if (activity == null) return;
|
||||||
|
activity.executeAfterFragmentResumed(new IExtendedActivity.Action() {
|
||||||
|
@Override
|
||||||
|
public void execute(IExtendedActivity activity) {
|
||||||
|
final FragmentManager fm = ((Activity) activity).getFragmentManager();
|
||||||
|
final DialogFragment fragment = (DialogFragment) fm.findFragmentByTag(PROGRESS_FRAGMENT_TAG);
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void showProgress() {
|
||||||
|
final MediaViewerActivity activity = (MediaViewerActivity) getActivity();
|
||||||
|
if (activity == null) return;
|
||||||
|
activity.executeAfterFragmentResumed(new IExtendedActivity.Action() {
|
||||||
|
@Override
|
||||||
|
public void execute(IExtendedActivity activity) {
|
||||||
|
final DialogFragment fragment = new ProgressDialogFragment();
|
||||||
|
fragment.setCancelable(false);
|
||||||
|
fragment.show(((Activity) activity).getFragmentManager(), PROGRESS_FRAGMENT_TAG);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onFileSaved(File savedFile, String mimeType) {
|
||||||
|
final MediaViewerActivity activity = (MediaViewerActivity) getActivity();
|
||||||
|
if (activity == null) return;
|
||||||
|
|
||||||
|
final Uri fileUri = FileProvider.getUriForFile(activity,
|
||||||
|
AUTHORITY_TWIDERE_FILE, savedFile);
|
||||||
|
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
intent.setDataAndType(fileUri, mimeType);
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
if (activity.hasStatus()) {
|
||||||
|
final ParcelableStatus status = activity.getStatus();
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, Utils.getStatusShareText(activity, status));
|
||||||
|
intent.putExtra(Intent.EXTRA_SUBJECT, Utils.getStatusShareSubject(activity, status));
|
||||||
|
}
|
||||||
|
startActivityForResult(Intent.createChooser(intent, activity.getString(R.string.share)),
|
||||||
|
REQUEST_SHARE_IMAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
task.execute();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
@ -484,7 +519,18 @@ public final class MediaViewerActivity extends BaseAppCompatActivity implements
|
||||||
loadImage();
|
loadImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||||
|
switch (requestCode) {
|
||||||
|
case REQUEST_SHARE_IMAGE: {
|
||||||
|
if (mShareImageFile != null) {
|
||||||
|
mShareImageFile.delete();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class ImagePageFragment extends BaseImagePageFragment
|
public static final class ImagePageFragment extends BaseImagePageFragment
|
||||||
|
@ -840,7 +886,13 @@ public final class MediaViewerActivity extends BaseAppCompatActivity implements
|
||||||
if (extension == null) return;
|
if (extension == null) return;
|
||||||
final File pubDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
final File pubDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||||
final File saveDir = new File(pubDir, "Twidere");
|
final File saveDir = new File(pubDir, "Twidere");
|
||||||
mSaveFileTask = AsyncTaskUtils.executeTask(new SaveFileTask(getActivity(), file, mimeType, saveDir));
|
mSaveFileTask = AsyncTaskUtils.executeTask(new ProgressSaveFileTask(getActivity(), file, saveDir,
|
||||||
|
new SaveFileTask.StringMimeTypeCallback(mimeType)) {
|
||||||
|
@Override
|
||||||
|
protected void onFileSaved(File savedFile, String mimeType) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -938,6 +990,7 @@ public final class MediaViewerActivity extends BaseAppCompatActivity implements
|
||||||
final Intent intent = new Intent(Intent.ACTION_SEND);
|
final Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
final Uri fileUri = Uri.fromFile(file);
|
final Uri fileUri = Uri.fromFile(file);
|
||||||
intent.setDataAndType(fileUri, linkAndType.second);
|
intent.setDataAndType(fileUri, linkAndType.second);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
|
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
|
||||||
final MediaViewerActivity activity = (MediaViewerActivity) getActivity();
|
final MediaViewerActivity activity = (MediaViewerActivity) getActivity();
|
||||||
if (activity.hasStatus()) {
|
if (activity.hasStatus()) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import android.os.Bundle;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||||
import android.support.v4.app.FragmentTrojan;
|
import android.support.v4.app.FragmentAccessor;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
public abstract class SupportFixedFragmentStatePagerAdapter extends FragmentStatePagerAdapter {
|
public abstract class SupportFixedFragmentStatePagerAdapter extends FragmentStatePagerAdapter {
|
||||||
|
@ -35,7 +35,7 @@ public abstract class SupportFixedFragmentStatePagerAdapter extends FragmentStat
|
||||||
@Override
|
@Override
|
||||||
public Object instantiateItem(final ViewGroup container, final int position) {
|
public Object instantiateItem(final ViewGroup container, final int position) {
|
||||||
final Fragment f = (Fragment) super.instantiateItem(container, position);
|
final Fragment f = (Fragment) super.instantiateItem(container, position);
|
||||||
final Bundle savedFragmentState = f != null ? FragmentTrojan.getSavedFragmentState(f) : null;
|
final Bundle savedFragmentState = f != null ? FragmentAccessor.getSavedFragmentState(f) : null;
|
||||||
if (savedFragmentState != null) {
|
if (savedFragmentState != null) {
|
||||||
savedFragmentState.setClassLoader(f.getClass().getClassLoader());
|
savedFragmentState.setClassLoader(f.getClass().getClassLoader());
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentActivity;
|
import android.support.v4.app.FragmentActivity;
|
||||||
import android.support.v4.app.FragmentManagerTrojan;
|
import android.support.v4.app.FragmentManagerAccessor;
|
||||||
import android.support.v4.view.LayoutInflaterCompat;
|
import android.support.v4.view.LayoutInflaterCompat;
|
||||||
import android.support.v4.view.LayoutInflaterFactory;
|
import android.support.v4.view.LayoutInflaterFactory;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
@ -193,7 +193,7 @@ public class BaseSupportFragment extends Fragment implements IBaseFragment, Cons
|
||||||
}
|
}
|
||||||
final LayoutInflater inflater = activity.getLayoutInflater().cloneInContext(getThemedContext());
|
final LayoutInflater inflater = activity.getLayoutInflater().cloneInContext(getThemedContext());
|
||||||
getChildFragmentManager(); // Init if needed; use raw implementation below.
|
getChildFragmentManager(); // Init if needed; use raw implementation below.
|
||||||
final LayoutInflaterFactory delegate = FragmentManagerTrojan.getLayoutInflaterFactory(getChildFragmentManager());
|
final LayoutInflaterFactory delegate = FragmentManagerAccessor.getLayoutInflaterFactory(getChildFragmentManager());
|
||||||
LayoutInflaterCompat.setFactory(inflater, new ThemedLayoutInflaterFactory((IThemedActivity) activity, delegate));
|
LayoutInflaterCompat.setFactory(inflater, new ThemedLayoutInflaterFactory((IThemedActivity) activity, delegate));
|
||||||
return inflater;
|
return inflater;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentActivity;
|
import android.support.v4.app.FragmentActivity;
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v4.app.FragmentManagerTrojan;
|
import android.support.v4.app.FragmentManagerAccessor;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
import android.support.v4.app.FragmentTransaction;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.AsyncTaskLoader;
|
import android.support.v4.content.AsyncTaskLoader;
|
||||||
|
@ -1167,7 +1167,7 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
|
||||||
}
|
}
|
||||||
final Fragment cardFragment = TwitterCardUtils.createCardFragment(status);
|
final Fragment cardFragment = TwitterCardUtils.createCardFragment(status);
|
||||||
final FragmentManager fm = fragment.getChildFragmentManager();
|
final FragmentManager fm = fragment.getChildFragmentManager();
|
||||||
if (cardFragment != null && !FragmentManagerTrojan.isStateSaved(fm)) {
|
if (cardFragment != null && !FragmentManagerAccessor.isStateSaved(fm)) {
|
||||||
final FragmentTransaction ft = fm.beginTransaction();
|
final FragmentTransaction ft = fm.beginTransaction();
|
||||||
ft.replace(R.id.twitter_card, cardFragment);
|
ft.replace(R.id.twitter_card, cardFragment);
|
||||||
ft.commit();
|
ft.commit();
|
||||||
|
|
|
@ -24,7 +24,7 @@ import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.content.AsyncTaskLoader;
|
import android.support.v4.content.AsyncTaskLoader;
|
||||||
import android.support.v4.content.LoaderTrojan;
|
import android.support.v4.content.LoaderAccessor;
|
||||||
|
|
||||||
import org.mariotaku.library.objectcursor.ObjectCursor;
|
import org.mariotaku.library.objectcursor.ObjectCursor;
|
||||||
|
|
||||||
|
@ -247,7 +247,7 @@ public class ObjectCursorLoader<T> extends AsyncTaskLoader<List<T>> {
|
||||||
writer.println(mObjects);
|
writer.println(mObjects);
|
||||||
writer.print(prefix);
|
writer.print(prefix);
|
||||||
writer.print("mContentChanged=");
|
writer.print("mContentChanged=");
|
||||||
writer.println(LoaderTrojan.isContentChanged(this));
|
writer.println(LoaderAccessor.isContentChanged(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Twidere - Twitter client for Android
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.mariotaku.twidere.task;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.DialogFragment;
|
||||||
|
import android.app.FragmentManager;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import org.mariotaku.twidere.activity.iface.IExtendedActivity;
|
||||||
|
import org.mariotaku.twidere.fragment.ProgressDialogFragment;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by mariotaku on 15/12/28.
|
||||||
|
*/
|
||||||
|
public abstract class ProgressSaveFileTask extends SaveFileTask {
|
||||||
|
private static final String PROGRESS_FRAGMENT_TAG = "progress";
|
||||||
|
|
||||||
|
public ProgressSaveFileTask(Context context, File source, File destination, MimeTypeCallback getMimeType) {
|
||||||
|
super(context, source, destination, getMimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void showProgress() {
|
||||||
|
final Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
((IExtendedActivity) context).executeAfterFragmentResumed(new IExtendedActivity.Action() {
|
||||||
|
@Override
|
||||||
|
public void execute(IExtendedActivity activity) {
|
||||||
|
final DialogFragment fragment = new ProgressDialogFragment();
|
||||||
|
fragment.setCancelable(false);
|
||||||
|
fragment.show(((Activity) activity).getFragmentManager(), PROGRESS_FRAGMENT_TAG);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void dismissProgress() {
|
||||||
|
final Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
((IExtendedActivity) context).executeAfterFragmentResumed(new IExtendedActivity.Action() {
|
||||||
|
@Override
|
||||||
|
public void execute(IExtendedActivity activity) {
|
||||||
|
final FragmentManager fm = ((Activity) activity).getFragmentManager();
|
||||||
|
final DialogFragment fragment = (DialogFragment) fm.findFragmentByTag(PROGRESS_FRAGMENT_TAG);
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
/*
|
||||||
|
* Twidere - Twitter client for Android
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.mariotaku.twidere.task;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import org.mariotaku.twidere.Constants;
|
||||||
|
import org.mariotaku.twidere.util.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
import okio.BufferedSink;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
|
||||||
|
public abstract class SaveFileTask extends AsyncTask<Object, Object, SaveFileTask.SaveFileResult> implements Constants {
|
||||||
|
|
||||||
|
private final WeakReference<Context> contextRef;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final File source, destination;
|
||||||
|
@NonNull
|
||||||
|
private final MimeTypeCallback getMimeType;
|
||||||
|
|
||||||
|
public SaveFileTask(@NonNull final Context context, @NonNull final File source,
|
||||||
|
@NonNull final File destination, @NonNull final MimeTypeCallback getMimeType) {
|
||||||
|
this.contextRef = new WeakReference<>(context);
|
||||||
|
this.source = source;
|
||||||
|
this.getMimeType = getMimeType;
|
||||||
|
this.destination = destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static SaveFileResult saveFile(@NonNull final Context context, @NonNull final File source,
|
||||||
|
@NonNull final MimeTypeCallback mimeTypeCallback,
|
||||||
|
@NonNull final File destinationDir) {
|
||||||
|
Source ioSrc = null;
|
||||||
|
BufferedSink sink = null;
|
||||||
|
try {
|
||||||
|
final String name = source.getName();
|
||||||
|
if (isEmpty(name)) return null;
|
||||||
|
final String mimeType = mimeTypeCallback.getMimeType(source);
|
||||||
|
final String extension = mimeTypeCallback.getExtension(mimeType);
|
||||||
|
if (extension == null) return null;
|
||||||
|
final String nameToSave = getFileNameWithExtension(name, extension);
|
||||||
|
if (!destinationDir.isDirectory() && !destinationDir.mkdirs()) return null;
|
||||||
|
final File saveFile = new File(destinationDir, nameToSave);
|
||||||
|
ioSrc = Okio.source(source);
|
||||||
|
sink = Okio.buffer(Okio.sink(saveFile));
|
||||||
|
sink.writeAll(ioSrc);
|
||||||
|
sink.flush();
|
||||||
|
return new SaveFileResult(saveFile, mimeType);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
Log.w(LOGTAG, "Failed to save file", e);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
Utils.closeSilently(sink);
|
||||||
|
Utils.closeSilently(ioSrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final SaveFileResult doInBackground(final Object... args) {
|
||||||
|
final Context context = contextRef.get();
|
||||||
|
if (context == null) return null;
|
||||||
|
return saveFile(context, source, getMimeType, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCancelled() {
|
||||||
|
dismissProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void onPreExecute() {
|
||||||
|
showProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void onPostExecute(@Nullable final SaveFileResult result) {
|
||||||
|
dismissProgress();
|
||||||
|
if (result != null) {
|
||||||
|
onFileSaved(result.savedFile, result.mimeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void onFileSaved(File savedFile, String mimeType);
|
||||||
|
|
||||||
|
protected abstract void showProgress();
|
||||||
|
|
||||||
|
protected abstract void dismissProgress();
|
||||||
|
|
||||||
|
|
||||||
|
protected final Context getContext() {
|
||||||
|
return contextRef.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getFileNameWithExtension(String name, String extension) {
|
||||||
|
int lastDotIdx = name.lastIndexOf('.');
|
||||||
|
if (lastDotIdx < 0) return name + "." + extension;
|
||||||
|
return name.substring(0, lastDotIdx) + "." + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface MimeTypeCallback {
|
||||||
|
String getMimeType(File source);
|
||||||
|
|
||||||
|
String getExtension(String mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class SaveFileResult {
|
||||||
|
File savedFile;
|
||||||
|
String mimeType;
|
||||||
|
|
||||||
|
public SaveFileResult(File savedFile, String mimeType) {
|
||||||
|
this.savedFile = savedFile;
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getSavedFile() {
|
||||||
|
return savedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMimeType() {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StringMimeTypeCallback implements MimeTypeCallback {
|
||||||
|
private final String mimeType;
|
||||||
|
|
||||||
|
public StringMimeTypeCallback(String mimeType) {
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMimeType(File source) {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getExtension(String mimeType) {
|
||||||
|
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Twidere - Twitter client for Android
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.mariotaku.twidere.task;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.MediaScannerConnection;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.mariotaku.twidere.R;
|
||||||
|
import org.mariotaku.twidere.util.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by mariotaku on 15/12/28.
|
||||||
|
*/
|
||||||
|
public class SaveImageToGalleryTask extends ProgressSaveFileTask {
|
||||||
|
|
||||||
|
public SaveImageToGalleryTask(@NonNull Activity activity, @NonNull File source, @NonNull File destination) {
|
||||||
|
super(activity, source, destination, new ImageMimeTypeCallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SaveFileTask create(final Activity activity, final File source) {
|
||||||
|
final File pubDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||||
|
final File saveDir = new File(pubDir, "Twidere");
|
||||||
|
return new SaveImageToGalleryTask(activity, source, saveDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onFileSaved(File savedFile, String mimeType) {
|
||||||
|
final Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
if (savedFile != null && savedFile.exists()) {
|
||||||
|
MediaScannerConnection.scanFile(context, new String[]{savedFile.getPath()},
|
||||||
|
new String[]{mimeType}, null);
|
||||||
|
Toast.makeText(context, R.string.saved_to_gallery, Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, R.string.error_occurred, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ImageMimeTypeCallback implements MimeTypeCallback {
|
||||||
|
@Override
|
||||||
|
public String getMimeType(File source) {
|
||||||
|
return Utils.getImageMimeType(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getExtension(String mimeType) {
|
||||||
|
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,151 +0,0 @@
|
||||||
/*
|
|
||||||
* Twidere - Twitter client for Android
|
|
||||||
*
|
|
||||||
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mariotaku.twidere.util;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.DialogFragment;
|
|
||||||
import android.app.FragmentManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.media.MediaScannerConnection;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.system.ErrnoException;
|
|
||||||
import android.system.OsConstants;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
|
||||||
import org.mariotaku.twidere.Constants;
|
|
||||||
import org.mariotaku.twidere.R;
|
|
||||||
import org.mariotaku.twidere.fragment.ProgressDialogFragment;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import okio.BufferedSink;
|
|
||||||
import okio.Okio;
|
|
||||||
import okio.Source;
|
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
|
|
||||||
public class SaveFileTask extends AsyncTask<Object, Object, File> implements Constants {
|
|
||||||
|
|
||||||
private static final String PROGRESS_FRAGMENT_TAG = "progress";
|
|
||||||
|
|
||||||
private final File source, destination;
|
|
||||||
private final Activity activity;
|
|
||||||
private final String mimeType;
|
|
||||||
|
|
||||||
public SaveFileTask(final Activity activity, final File source, final String mimeType, final File destination) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.source = source;
|
|
||||||
this.mimeType = mimeType;
|
|
||||||
this.destination = destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SaveFileTask saveImage(final Activity activity, final File source) {
|
|
||||||
final String mimeType = Utils.getImageMimeType(source);
|
|
||||||
final MimeTypeMap map = MimeTypeMap.getSingleton();
|
|
||||||
final String extension = map.getExtensionFromMimeType(mimeType);
|
|
||||||
if (extension == null) return null;
|
|
||||||
final File pubDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
|
||||||
final File saveDir = new File(pubDir, "Twidere");
|
|
||||||
return new SaveFileTask(activity, source, mimeType, saveDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static File saveFile(final Context context, final File source, final String mimeType, final File destination) {
|
|
||||||
if (context == null && source == null) return null;
|
|
||||||
Source ioSrc = null;
|
|
||||||
BufferedSink sink = null;
|
|
||||||
try {
|
|
||||||
final String name = source.getName();
|
|
||||||
if (isEmpty(name)) return null;
|
|
||||||
final MimeTypeMap map = MimeTypeMap.getSingleton();
|
|
||||||
final String extension = map.getExtensionFromMimeType(mimeType);
|
|
||||||
if (extension == null) return null;
|
|
||||||
final String nameToSave = getFileNameWithExtension(name, extension);
|
|
||||||
if (!destination.isDirectory() && !destination.mkdirs()) return null;
|
|
||||||
final File saveFile = new File(destination, nameToSave);
|
|
||||||
ioSrc = Okio.source(source);
|
|
||||||
sink = Okio.buffer(Okio.sink(saveFile));
|
|
||||||
sink.writeAll(ioSrc);
|
|
||||||
sink.flush();
|
|
||||||
if (mimeType != null) {
|
|
||||||
MediaScannerConnection.scanFile(context, new String[]{saveFile.getPath()},
|
|
||||||
new String[]{mimeType}, null);
|
|
||||||
}
|
|
||||||
return saveFile;
|
|
||||||
} catch (final IOException e) {
|
|
||||||
final int errno = Utils.getErrorNo(e.getCause());
|
|
||||||
Log.w(LOGTAG, "Failed to save file", e);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
Utils.closeSilently(sink);
|
|
||||||
Utils.closeSilently(ioSrc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getFileNameWithExtension(String name, String extension) {
|
|
||||||
int lastDotIdx = name.lastIndexOf('.');
|
|
||||||
if (lastDotIdx < 0) return name + "." + extension;
|
|
||||||
return name.substring(0, lastDotIdx) + "." + extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected File doInBackground(final Object... args) {
|
|
||||||
if (source == null) return null;
|
|
||||||
return saveFile(activity, source, mimeType, destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCancelled() {
|
|
||||||
final FragmentManager fm = activity.getFragmentManager();
|
|
||||||
final DialogFragment fragment = (DialogFragment) fm.findFragmentByTag(PROGRESS_FRAGMENT_TAG);
|
|
||||||
if (fragment != null && fragment.isVisible()) {
|
|
||||||
fragment.dismiss();
|
|
||||||
}
|
|
||||||
super.onCancelled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(final File result) {
|
|
||||||
final FragmentManager fm = activity.getFragmentManager();
|
|
||||||
final DialogFragment fragment = (DialogFragment) fm.findFragmentByTag(PROGRESS_FRAGMENT_TAG);
|
|
||||||
if (fragment != null) {
|
|
||||||
fragment.dismiss();
|
|
||||||
}
|
|
||||||
super.onPostExecute(result);
|
|
||||||
if (result != null && result.exists()) {
|
|
||||||
Toast.makeText(activity, R.string.saved_to_gallery, Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(activity, R.string.error_occurred, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
final DialogFragment fragment = new ProgressDialogFragment();
|
|
||||||
fragment.setCancelable(false);
|
|
||||||
fragment.show(activity.getFragmentManager(), PROGRESS_FRAGMENT_TAG);
|
|
||||||
super.onPreExecute();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -34,7 +34,7 @@ import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.support.v7.app.AppCompatDelegate;
|
import android.support.v7.app.AppCompatDelegate;
|
||||||
import android.support.v7.app.AppCompatDelegateTrojan;
|
import android.support.v7.app.AppCompatDelegateAccessor;
|
||||||
import android.support.v7.view.ContextThemeWrapper;
|
import android.support.v7.view.ContextThemeWrapper;
|
||||||
import android.support.v7.widget.TwidereToolbar;
|
import android.support.v7.widget.TwidereToolbar;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
@ -257,13 +257,13 @@ public class ThemedLayoutInflaterFactory implements LayoutInflaterFactory {
|
||||||
Context actionBarContext = null;
|
Context actionBarContext = null;
|
||||||
if (activity instanceof AppCompatActivity) {
|
if (activity instanceof AppCompatActivity) {
|
||||||
final AppCompatDelegate delegate = ((AppCompatActivity) activity).getDelegate();
|
final AppCompatDelegate delegate = ((AppCompatActivity) activity).getDelegate();
|
||||||
final ActionBar actionBar = AppCompatDelegateTrojan.peekActionBar(delegate);
|
final ActionBar actionBar = AppCompatDelegateAccessor.peekActionBar(delegate);
|
||||||
if (actionBar != null) {
|
if (actionBar != null) {
|
||||||
actionBarContext = actionBar.getThemedContext();
|
actionBarContext = actionBar.getThemedContext();
|
||||||
}
|
}
|
||||||
} else if (activity instanceof AppCompatPreferenceActivity) {
|
} else if (activity instanceof AppCompatPreferenceActivity) {
|
||||||
final AppCompatDelegate delegate = ((AppCompatPreferenceActivity) activity).getDelegate();
|
final AppCompatDelegate delegate = ((AppCompatPreferenceActivity) activity).getDelegate();
|
||||||
final ActionBar actionBar = AppCompatDelegateTrojan.peekActionBar(delegate);
|
final ActionBar actionBar = AppCompatDelegateAccessor.peekActionBar(delegate);
|
||||||
if (actionBar != null) {
|
if (actionBar != null) {
|
||||||
actionBarContext = actionBar.getThemedContext();
|
actionBarContext = actionBar.getThemedContext();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<bool name="shadow_slidable">false</bool>
|
<bool name="shadow_slidable">false</bool>
|
||||||
<bool name="is_large_screen">true</bool>
|
|
||||||
<bool name="relative_behind_width">false</bool>
|
<bool name="relative_behind_width">false</bool>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
<bool name="default_display_tab_label">false</bool>
|
<bool name="default_display_tab_label">false</bool>
|
||||||
<bool name="home_display_icon">false</bool>
|
<bool name="home_display_icon">false</bool>
|
||||||
<bool name="is_large_screen">false</bool>
|
|
||||||
<bool name="default_shadow_slidable">true</bool>
|
<bool name="default_shadow_slidable">true</bool>
|
||||||
<bool name="shadow_slidable">true</bool>
|
<bool name="shadow_slidable">true</bool>
|
||||||
<bool name="has_font_family">false</bool>
|
<bool name="has_font_family">false</bool>
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Twidere - Twitter client for Android
|
||||||
|
~
|
||||||
|
~ Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||||
|
~
|
||||||
|
~ This program is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by
|
||||||
|
~ the Free Software Foundation, either version 3 of the License, or
|
||||||
|
~ (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<paths>
|
||||||
|
<cache-path
|
||||||
|
name="shares"
|
||||||
|
path="shared_files/"/>
|
||||||
|
</paths>
|
Loading…
Reference in New Issue