diff options
Diffstat (limited to 'main/src/ui/java')
46 files changed, 13187 insertions, 0 deletions
diff --git a/main/src/ui/java/android/support/v4n/app/FragmentStatePagerAdapter.java b/main/src/ui/java/android/support/v4n/app/FragmentStatePagerAdapter.java new file mode 100644 index 00000000..4ffec519 --- /dev/null +++ b/main/src/ui/java/android/support/v4n/app/FragmentStatePagerAdapter.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4n.app; + +import java.util.ArrayList; + +import android.annotation.TargetApi; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4n.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * Implementation of {@link android.support.v4.view.PagerAdapter} that + * uses a {@link Fragment} to manage each page. This class also handles + * saving and restoring of fragment's state. + * + * <p>This version of the pager is more useful when there are a large number + * of pages, working more like a list view. When pages are not visible to + * the user, their entire fragment may be destroyed, only keeping the saved + * state of that fragment. This allows the pager to hold on to much less + * memory associated with each visited page as compared to + * {@link FragmentPagerAdapter} at the cost of potentially more overhead when + * switching between pages. + * + * <p>When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.</p> + * + * <p>Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + * <p>Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java + * complete} + * + * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml + * complete} + * + * <p>The <code>R.layout.fragment_pager_list</code> resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentStatePagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentStatePagerAdptr"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + + private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>(); + private ArrayList<Fragment> mFragments = new ArrayList<Fragment>(); + private Fragment mCurrentPrimaryItem = null; + + public FragmentStatePagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + @Override + public Object instantiateItem(ViewGroup container, int position) { + // If we already have this item instantiated, there is nothing + // to do. This can happen when we are restoring the entire pager + // from its saved state, where the fragment manager has already + // taken care of restoring the fragments we previously had instantiated. + if (mFragments.size() > position) { + Fragment f = mFragments.get(position); + if (f != null) { + return f; + } + } + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + Fragment fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); + if (mSavedState.size() > position) { + Fragment.SavedState fss = mSavedState.get(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + } + while (mFragments.size() <= position) { + mFragments.add(null); + } + fragment.setMenuVisibility(false); + fragment.setUserVisibleHint(false); + mFragments.set(position, fragment); + mCurTransaction.add(container.getId(), fragment); + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + + " v=" + ((Fragment)object).getView()); + while (mSavedState.size() <= position) { + mSavedState.add(null); + } + mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); + mFragments.set(position, null); + + mCurTransaction.remove(fragment); + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + mCurrentPrimaryItem.setUserVisibleHint(false); + } + if (fragment != null) { + fragment.setMenuVisibility(true); + fragment.setUserVisibleHint(true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + Bundle state = null; + if (mSavedState.size() > 0) { + state = new Bundle(); + Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + mSavedState.toArray(fss); + state.putParcelableArray("states", fss); + } + for (int i=0; i<mFragments.size(); i++) { + Fragment f = mFragments.get(i); + if (f != null) { + if (state == null) { + state = new Bundle(); + } + String key = "f" + i; + mFragmentManager.putFragment(state, key, f); + } + } + return state; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + if (state != null) { + Bundle bundle = (Bundle)state; + bundle.setClassLoader(loader); + Parcelable[] fss = bundle.getParcelableArray("states"); + mSavedState.clear(); + mFragments.clear(); + if (fss != null) { + for (int i=0; i<fss.length; i++) { + mSavedState.add((Fragment.SavedState)fss[i]); + } + } + Iterable<String> keys = bundle.keySet(); + for (String key: keys) { + if (key.startsWith("f")) { + int index = Integer.parseInt(key.substring(1)); + Fragment f = mFragmentManager.getFragment(bundle, key); + if (f != null) { + while (mFragments.size() <= index) { + mFragments.add(null); + } + f.setMenuVisibility(false); + mFragments.set(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } + } + } + } + } +} diff --git a/main/src/ui/java/android/support/v4n/view/PagerAdapter.java b/main/src/ui/java/android/support/v4n/view/PagerAdapter.java new file mode 100644 index 00000000..70ed75f3 --- /dev/null +++ b/main/src/ui/java/android/support/v4n/view/PagerAdapter.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4n.view; + +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +/** + * Base class providing the adapter to populate pages inside of + * a {@link ViewPager}. You will most likely want to use a more + * specific implementation of this, such as + * {@link android.support.v4n.app.FragmentPagerAdapter} or + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + * + * <p>When you implement a PagerAdapter, you must override the following methods + * at minimum:</p> + * <ul> + * <li>{@link #instantiateItem(ViewGroup, int)}</li> + * <li>{@link #destroyItem(ViewGroup, int, Object)}</li> + * <li>{@link #getCount()}</li> + * <li>{@link #isViewFromObject(View, Object)}</li> + * </ul> + * + * <p>PagerAdapter is more general than the adapters used for + * {@link android.widget.AdapterView AdapterViews}. Instead of providing a + * View recycling mechanism directly ViewPager uses callbacks to indicate the + * steps taken during an update. A PagerAdapter may implement a form of View + * recycling if desired or use a more sophisticated method of managing page + * Views such as Fragment transactions where each page is represented by its + * own Fragment.</p> + * + * <p>ViewPager associates each page with a key Object instead of working with + * Views directly. This key is used to track and uniquely identify a given page + * independent of its position in the adapter. A call to the PagerAdapter method + * {@link #startUpdate(ViewGroup)} indicates that the contents of the ViewPager + * are about to change. One or more calls to {@link #instantiateItem(ViewGroup, int)} + * and/or {@link #destroyItem(ViewGroup, int, Object)} will follow, and the end + * of an update will be signaled by a call to {@link #finishUpdate(ViewGroup)}. + * By the time {@link #finishUpdate(ViewGroup) finishUpdate} returns the views + * associated with the key objects returned by + * {@link #instantiateItem(ViewGroup, int) instantiateItem} should be added to + * the parent ViewGroup passed to these methods and the views associated with + * the keys passed to {@link #destroyItem(ViewGroup, int, Object) destroyItem} + * should be removed. The method {@link #isViewFromObject(View, Object)} identifies + * whether a page View is associated with a given key object.</p> + * + * <p>A very simple PagerAdapter may choose to use the page Views themselves + * as key objects, returning them from {@link #instantiateItem(ViewGroup, int)} + * after creation and adding them to the parent ViewGroup. A matching + * {@link #destroyItem(ViewGroup, int, Object)} implementation would remove the + * View from the parent ViewGroup and {@link #isViewFromObject(View, Object)} + * could be implemented as <code>return view == object;</code>.</p> + * + * <p>PagerAdapter supports data set changes. Data set changes must occur on the + * main thread and must end with a call to {@link #notifyDataSetChanged()} similar + * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data + * set change may involve pages being added, removed, or changing position. The + * ViewPager will keep the current page active provided the adapter implements + * the method {@link #getItemPosition(Object)}.</p> + */ +public abstract class PagerAdapter { + private DataSetObservable mObservable = new DataSetObservable(); + + public static final int POSITION_UNCHANGED = -1; + public static final int POSITION_NONE = -2; + + /** + * Return the number of views available. + */ + public abstract int getCount(); + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void startUpdate(ViewGroup container) { + startUpdate((View) container); + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + */ + public Object instantiateItem(ViewGroup container, int position) { + return instantiateItem((View) container, position); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + */ + public void destroyItem(ViewGroup container, int position, Object object) { + destroyItem((View) container, position, object); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + */ + public void setPrimaryItem(ViewGroup container, int position, Object object) { + setPrimaryItem((View) container, position, object); + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void finishUpdate(ViewGroup container) { + finishUpdate((View) container); + } + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #startUpdate(ViewGroup)} + */ + public void startUpdate(View container) { + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + * + * @deprecated Use {@link #instantiateItem(ViewGroup, int)} + */ + public Object instantiateItem(View container, int position) { + throw new UnsupportedOperationException( + "Required method instantiateItem was not overridden"); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(View)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + * + * @deprecated Use {@link #destroyItem(ViewGroup, int, Object)} + */ + public void destroyItem(View container, int position, Object object) { + throw new UnsupportedOperationException("Required method destroyItem was not overridden"); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + * + * @deprecated Use {@link #setPrimaryItem(ViewGroup, int, Object)} + */ + public void setPrimaryItem(View container, int position, Object object) { + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #finishUpdate(ViewGroup)} + */ + public void finishUpdate(View container) { + } + + /** + * Determines whether a page View is associated with a specific key object + * as returned by {@link #instantiateItem(ViewGroup, int)}. This method is + * required for a PagerAdapter to function properly. + * + * @param view Page View to check for association with <code>object</code> + * @param object Object to check for association with <code>view</code> + * @return true if <code>view</code> is associated with the key object <code>object</code> + */ + public abstract boolean isViewFromObject(View view, Object object); + + /** + * Save any instance state associated with this adapter and its pages that should be + * restored if the current UI state needs to be reconstructed. + * + * @return Saved state for this adapter + */ + public Parcelable saveState() { + return null; + } + + /** + * Restore any instance state associated with this adapter and its pages + * that was previously saved by {@link #saveState()}. + * + * @param state State previously saved by a call to {@link #saveState()} + * @param loader A ClassLoader that should be used to instantiate any restored objects + */ + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Called when the host view is attempting to determine if an item's position + * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given + * item has not changed or {@link #POSITION_NONE} if the item is no longer present + * in the adapter. + * + * <p>The default implementation assumes that items will never + * change position and always returns {@link #POSITION_UNCHANGED}. + * + * @param object Object representing an item, previously returned by a call to + * {@link #instantiateItem(View, int)}. + * @return object's new position index from [0, {@link #getCount()}), + * {@link #POSITION_UNCHANGED} if the object's position has not changed, + * or {@link #POSITION_NONE} if the item is no longer present. + */ + public int getItemPosition(Object object) { + return POSITION_UNCHANGED; + } + + /** + * This method should be called by the application if the data backing this adapter has changed + * and associated views should update. + */ + public void notifyDataSetChanged() { + mObservable.notifyChanged(); + } + + /** + * Register an observer to receive callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will receive callbacks. + */ + public void registerDataSetObserver(DataSetObserver observer) { + mObservable.registerObserver(observer); + } + + /** + * Unregister an observer from callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will be unregistered. + */ + public void unregisterDataSetObserver(DataSetObserver observer) { + mObservable.unregisterObserver(observer); + } + + /** + * This method may be called by the ViewPager to obtain a title string + * to describe the specified page. This method may return null + * indicating no title for this page. The default implementation returns + * null. + * + * @param position The position of the title requested + * @return A title for the requested page + */ + public CharSequence getPageTitle(int position) { + return null; + } + + /** + * Returns the proportional width of a given page as a percentage of the + * ViewPager's measured width from (0.f-1.f] + * + * @param position The position of the page requested + * @return Proportional width for the given page position + */ + public float getPageWidth(int position) { + return 1.f; + } +} diff --git a/main/src/ui/java/android/support/v4n/view/ViewPager.java b/main/src/ui/java/android/support/v4n/view/ViewPager.java new file mode 100644 index 00000000..6009af62 --- /dev/null +++ b/main/src/ui/java/android/support/v4n/view/ViewPager.java @@ -0,0 +1,2895 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4n.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.DrawableRes; +import android.support.v4.os.ParcelableCompat; +import android.support.v4.os.ParcelableCompatCreatorCallbacks; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewConfigurationCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.support.v4.widget.EdgeEffectCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * Layout manager that allows the user to flip left and right + * through pages of data. You supply an implementation of a + * {@link PagerAdapter} to generate the pages that the view shows. + * + * <p>Note this class is currently under early design and + * development. The API will likely change in later updates of + * the compatibility library, requiring changes to the source code + * of apps when they are compiled against the newer version.</p> + * + * <p>ViewPager is most often used in conjunction with {@link android.app.Fragment}, + * which is a convenient way to supply and manage the lifecycle of each page. + * There are standard adapters implemented for using fragments with the ViewPager, + * which cover the most common use cases. These are + * {@link android.support.v4n.app.FragmentPagerAdapter} and + * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these + * classes have simple code showing how to build a full user interface + * with them. + * + * <p>For more information about how to use ViewPager, read <a + * href="{@docRoot}training/implementing-navigation/lateral.html">Creating Swipe Views with + * Tabs</a>.</p> + * + * <p>Below is a more complicated example of ViewPager, using it in conjunction + * with {@link android.app.ActionBar} tabs. You can find other examples of using + * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code. + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java + * complete} + */ +public class ViewPager extends ViewGroup { + private static final String TAG = "ViewPager"; + private static final boolean DEBUG = false; + + private static final boolean USE_CACHE = false; + + private static final int DEFAULT_OFFSCREEN_PAGES = 1; + private static final int MAX_SETTLE_DURATION = 600; // ms + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips + + private static final int DEFAULT_GUTTER_SIZE = 16; // dips + + private static final int MIN_FLING_VELOCITY = 400; // dips + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + /** + * Used to track what the expected number of items in the adapter should be. + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. + */ + private int mExpectedAdapterCount; + + static class ItemInfo { + Object object; + int position; + boolean scrolling; + float widthFactor; + float offset; + } + + private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){ + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return lhs.position - rhs.position; + } + }; + + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); + private final ItemInfo mTempItem = new ItemInfo(); + + private final Rect mTempRect = new Rect(); + + private PagerAdapter mAdapter; + private int mCurItem; // Index of currently displayed page. + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + private Scroller mScroller; + private PagerObserver mObserver; + + private int mPageMargin; + private Drawable mMarginDrawable; + private int mTopPageBounds; + private int mBottomPageBounds; + + // Offsets of the first and last items, if known. + // Set during population, used to determine if we are at the beginning + // or end of the pager data set during touch scrolling. + private float mFirstOffset = -Float.MAX_VALUE; + private float mLastOffset = Float.MAX_VALUE; + + private int mChildWidthMeasureSpec; + private int mChildHeightMeasureSpec; + private boolean mInLayout; + + private boolean mScrollingCacheEnabled; + + private boolean mPopulatePending; + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; + + private boolean mIsBeingDragged; + private boolean mIsUnableToDrag; + private boolean mIgnoreGutter; + private int mDefaultGutterSize; + private int mGutterSize; + private int mTouchSlop; + /** + * Position of the last motion event. + */ + private float mLastMotionX; + private float mLastMotionY; + private float mInitialMotionX; + private float mInitialMotionY; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mFlingDistance; + private int mCloseEnough; + + // If the pager is at least this close to its final position, complete the scroll + // on touch down and let the user interact with the content inside instead of + // "catching" the flinging pager. + private static final int CLOSE_ENOUGH = 2; // dp + + private boolean mFakeDragging; + private long mFakeDragBeginTime; + + private EdgeEffectCompat mLeftEdge; + private EdgeEffectCompat mRightEdge; + + private boolean mFirstLayout = true; + private boolean mNeedCalculatePageOffsets = false; + private boolean mCalledSuper; + private int mDecorChildCount; + + private OnPageChangeListener mOnPageChangeListener; + private OnPageChangeListener mInternalPageChangeListener; + private OnAdapterChangeListener mAdapterChangeListener; + private PageTransformer mPageTransformer; + private Method mSetChildrenDrawingOrderEnabled; + + private static final int DRAW_ORDER_DEFAULT = 0; + private static final int DRAW_ORDER_FORWARD = 1; + private static final int DRAW_ORDER_REVERSE = 2; + private int mDrawingOrder; + private ArrayList<View> mDrawingOrderedChildren; + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); + + /** + * Indicates that the pager is in an idle, settled state. The current page + * is fully in view and no animation is in progress. + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * Indicates that the pager is currently being dragged by the user. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * Indicates that the pager is in the process of settling to a final position. + */ + public static final int SCROLL_STATE_SETTLING = 2; + + private final Runnable mEndScrollRunnable = new Runnable() { + public void run() { + setScrollState(SCROLL_STATE_IDLE); + populate(); + } + }; + + private int mScrollState = SCROLL_STATE_IDLE; + + /** + * Callback interface for responding to changing state of the selected page. + */ + public interface OnPageChangeListener { + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + public void onPageSelected(int position); + + /** + * Called when the scroll state changes. Useful for discovering when the user + * begins dragging, when the pager is automatically settling to the current page, + * or when it is fully stopped/idle. + * + * @param state The new scroll state. + * @see ViewPager#SCROLL_STATE_IDLE + * @see ViewPager#SCROLL_STATE_DRAGGING + * @see ViewPager#SCROLL_STATE_SETTLING + */ + public void onPageScrollStateChanged(int state); + } + + /** + * Simple implementation of the {@link OnPageChangeListener} interface with stub + * implementations of each method. Extend this if you do not intend to override + * every method of {@link OnPageChangeListener}. + */ + public static class SimpleOnPageChangeListener implements OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // This space for rent + } + + @Override + public void onPageSelected(int position) { + // This space for rent + } + + @Override + public void onPageScrollStateChanged(int state) { + // This space for rent + } + } + + /** + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. + * + * <p>As property animation is only supported as of Android 3.0 and forward, + * setting a PageTransformer on a ViewPager on earlier platform versions will + * be ignored.</p> + */ + public interface PageTransformer { + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -1 is one page position to the left. + */ + public void transformPage(View page, float position); + } + + /** + * Used internally to monitor when adapters are switched. + */ + interface OnAdapterChangeListener { + public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); + } + + /** + * Used internally to tag special types of child views that should be added as + * pager decorations by default. + */ + interface Decor {} + + public ViewPager(Context context) { + super(context); + initViewPager(); + } + + public ViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + initViewPager(); + } + + void initViewPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + final Context context = getContext(); + mScroller = new Scroller(context, sInterpolator); + final ViewConfiguration configuration = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mLeftEdge = new EdgeEffectCompat(context); + mRightEdge = new EdgeEffectCompat(context); + + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); + mCloseEnough = (int) (CLOSE_ENOUGH * density); + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); + + ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); + + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mEndScrollRunnable); + super.onDetachedFromWindow(); + } + + private void setScrollState(int newState) { + if (mScrollState == newState) { + return; + } + + mScrollState = newState; + if (mPageTransformer != null) { + // PageTransformers can do complex things that benefit from hardware layers. + enableLayers(newState != SCROLL_STATE_IDLE); + } + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(newState); + } + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(PagerAdapter adapter) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + mAdapter.startUpdate(this); + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + mAdapter.destroyItem(this, ii.position, ii.object); + } + mAdapter.finishUpdate(this); + mItems.clear(); + removeNonDecorViews(); + mCurItem = 0; + scrollTo(0, 0); + } + + final PagerAdapter oldAdapter = mAdapter; + mAdapter = adapter; + mExpectedAdapterCount = 0; + + if (mAdapter != null) { + if (mObserver == null) { + mObserver = new PagerObserver(); + } + mAdapter.registerDataSetObserver(mObserver); + mPopulatePending = false; + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + mExpectedAdapterCount = mAdapter.getCount(); + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, false, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + + if (mAdapterChangeListener != null && oldAdapter != adapter) { + mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); + } + } + + private void removeNonDecorViews() { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + removeViewAt(i); + i--; + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + public PagerAdapter getAdapter() { + return mAdapter; + } + + void setOnAdapterChangeListener(OnAdapterChangeListener listener) { + mAdapterChangeListener = listener; + } + + private int getClientWidth() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * Set the currently selected page. If the ViewPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + mPopulatePending = false; + setCurrentItemInternal(item, !mFirstLayout, false); + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately + */ + public void setCurrentItem(int item, boolean smoothScroll) { + mPopulatePending = false; + setCurrentItemInternal(item, smoothScroll, false); + } + + public int getCurrentItem() { + return mCurItem; + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { + setCurrentItemInternal(item, smoothScroll, always, 0); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + setScrollingCacheEnabled(false); + return; + } + if (!always && mCurItem == item && mItems.size() != 0) { + setScrollingCacheEnabled(false); + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final int pageLimit = mOffscreenPageLimit; + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { + // We are doing a jump by more than one page. To avoid + // glitches, we want to keep all current pages in the view + // until the scroll ends. + for (int i=0; i<mItems.size(); i++) { + mItems.get(i).scrolling = true; + } + } + final boolean dispatchSelected = mCurItem != item; + + if (mFirstLayout) { + // We don't have any idea how big we are yet and shouldn't have any pages either. + // Just set things up and let the pending layout handle things. + mCurItem = item; + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + requestLayout(); + } else { + populate(item); + scrollToItem(item, smoothScroll, velocity, dispatchSelected); + } + } + + private void scrollToItem(int item, boolean smoothScroll, int velocity, + boolean dispatchSelected) { + final ItemInfo curInfo = infoForPosition(item); + int destX = 0; + if (curInfo != null) { + final int width = getClientWidth(); + destX = (int) (width * Math.max(mFirstOffset, + Math.min(curInfo.offset, mLastOffset))); + } + if (smoothScroll) { + smoothScrollTo(destX, 0, velocity); + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + } else { + if (dispatchSelected && mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(item); + } + if (dispatchSelected && mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(item); + } + completeScroll(false); + scrollTo(destX, 0); + pageScrolled(destX); + } + } + + /** + * Set a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link OnPageChangeListener}. + * + * @param listener Listener to set + */ + public void setOnPageChangeListener(OnPageChangeListener listener) { + mOnPageChangeListener = listener; + } + + /** + * Set a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding look and feel. + * + * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist. + * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p> + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + */ + public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { + if (Build.VERSION.SDK_INT >= 11) { + final boolean hasTransformer = transformer != null; + final boolean needsPopulate = hasTransformer != (mPageTransformer != null); + mPageTransformer = transformer; + setChildrenDrawingOrderEnabledCompat(hasTransformer); + if (hasTransformer) { + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; + } else { + mDrawingOrder = DRAW_ORDER_DEFAULT; + } + if (needsPopulate) populate(); + } + } + + void setChildrenDrawingOrderEnabledCompat(boolean enable) { + if (Build.VERSION.SDK_INT >= 7) { + if (mSetChildrenDrawingOrderEnabled == null) { + try { + mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( + "setChildrenDrawingOrderEnabled", new Class[] { Boolean.TYPE }); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); + } + } + try { + mSetChildrenDrawingOrderEnabled.invoke(this, enable); + } catch (Exception e) { + Log.e(TAG, "Error changing children drawing order", e); + } + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; + final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; + return result; + } + + /** + * Set a separate OnPageChangeListener for internal use by the support library. + * + * @param listener Listener to set + * @return The old listener that was set, if any. + */ + OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { + OnPageChangeListener oldListener = mInternalPageChangeListener; + mInternalPageChangeListener = listener; + return oldListener; + } + + /** + * Returns the number of pages that will be retained to either side of the + * current page in the view hierarchy in an idle state. Defaults to 1. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + * <p>This is offered as an optimization. If you know in advance the number + * of pages you will need to support or have lazy-loading mechanisms in place + * on your pages, tweaking this setting can have benefits in perceived smoothness + * of paging animations and interaction. If you have a small number of pages (3-4) + * that you can keep active all at once, less time will be spent in layout for + * newly created view subtrees as the user pages back and forth.</p> + * + * <p>You should keep this limit low, especially if your pages have complex layouts. + * This setting defaults to 1.</p> + * + * @param limit How many pages will be kept offscreen in an idle state. + */ + public void setOffscreenPageLimit(int limit) { + if (limit < DEFAULT_OFFSCREEN_PAGES) { + Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + + DEFAULT_OFFSCREEN_PAGES); + limit = DEFAULT_OFFSCREEN_PAGES; + } + if (limit != mOffscreenPageLimit) { + mOffscreenPageLimit = limit; + populate(); + } + } + + /** + * Set the margin between pages. + * + * @param marginPixels Distance between adjacent pages in pixels + * @see #getPageMargin() + * @see #setPageMarginDrawable(Drawable) + * @see #setPageMarginDrawable(int) + */ + public void setPageMargin(int marginPixels) { + final int oldMargin = mPageMargin; + mPageMargin = marginPixels; + + final int width = getWidth(); + recomputeScrollPosition(width, width, marginPixels, oldMargin); + + requestLayout(); + } + + /** + * Return the margin between pages. + * + * @return The size of the margin in pixels + */ + public int getPageMargin() { + return mPageMargin; + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param d Drawable to display between pages + */ + public void setPageMarginDrawable(Drawable d) { + mMarginDrawable = d; + if (d != null) refreshDrawableState(); + setWillNotDraw(d == null); + invalidate(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param resId Resource ID of a drawable to display between pages + */ + public void setPageMarginDrawable(@DrawableRes int resId) { + setPageMarginDrawable(getContext().getResources().getDrawable(resId)); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mMarginDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final Drawable d = mMarginDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + */ + void smoothScrollTo(int x, int y) { + smoothScrollTo(x, y, 0); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) + */ + void smoothScrollTo(int x, int y, int velocity) { + if (getChildCount() == 0) { + // Nothing to do. + setScrollingCacheEnabled(false); + return; + } + int sx = getScrollX(); + int sy = getScrollY(); + int dx = x - sx; + int dy = y - sy; + if (dx == 0 && dy == 0) { + completeScroll(false); + populate(); + setScrollState(SCROLL_STATE_IDLE); + return; + } + + setScrollingCacheEnabled(true); + setScrollState(SCROLL_STATE_SETTLING); + + final int width = getClientWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration = 0; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageWidth = width * mAdapter.getPageWidth(mCurItem); + final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + mScroller.startScroll(sx, sy, dx, dy, duration); + ViewCompat.postInvalidateOnAnimation(this); + } + + ItemInfo addNewItem(int position, int index) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + ii.widthFactor = mAdapter.getPageWidth(position); + if (index < 0 || index >= mItems.size()) { + mItems.add(ii); + } else { + mItems.add(index, ii); + } + return ii; + } + + void dataSetChanged() { + // This method only gets called if our observer is attached, so mAdapter is non-null. + + final int adapterCount = mAdapter.getCount(); + mExpectedAdapterCount = adapterCount; + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && + mItems.size() < adapterCount; + int newCurrItem = mCurItem; + + boolean isUpdating = false; + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + final int newPos = mAdapter.getItemPosition(ii.object); + + if (newPos == PagerAdapter.POSITION_UNCHANGED) { + continue; + } + + if (newPos == PagerAdapter.POSITION_NONE) { + mItems.remove(i); + i--; + + if (!isUpdating) { + mAdapter.startUpdate(this); + isUpdating = true; + } + + mAdapter.destroyItem(this, ii.position, ii.object); + needPopulate = true; + + if (mCurItem == ii.position) { + // Keep the current item in the valid range + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + needPopulate = true; + } + continue; + } + + if (ii.position != newPos) { + if (ii.position == mCurItem) { + // Our current item changed position. Follow it. + newCurrItem = newPos; + } + + ii.position = newPos; + needPopulate = true; + } + } + + if (isUpdating) { + mAdapter.finishUpdate(this); + } + + Collections.sort(mItems, COMPARATOR); + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + lp.widthFactor = 0.f; + } + } + + setCurrentItemInternal(newCurrItem, false, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + ItemInfo oldCurInfo = null; + int focusDirection = View.FOCUS_FORWARD; + if (mCurItem != newCurrentItem) { + focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + oldCurInfo = infoForPosition(mCurItem); + mCurItem = newCurrentItem; + } + + if (mAdapter == null) { + sortChildDrawingOrder(); + return; + } + + // Bail now if we are waiting to populate. This is to hold off + // on creating views from the time the user releases their finger to + // fling to a new position until we have finished the scroll to + // that position, avoiding glitches from happening at that point. + if (mPopulatePending) { + if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); + sortChildDrawingOrder(); + return; + } + + // Also, don't populate until we are attached to a window. This is to + // avoid trying to populate before we have restored our view hierarchy + // state and conflicting with what is restored. + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + final int pageLimit = mOffscreenPageLimit; + final int startPos = Math.max(0, mCurItem - pageLimit); + final int N = mAdapter.getCount(); + final int endPos = Math.min(N-1, mCurItem + pageLimit); + + if (N != mExpectedAdapterCount) { + String resName; + try { + resName = getResources().getResourceName(getId()); + } catch (Resources.NotFoundException e) { + resName = Integer.toHexString(getId()); + } + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + + " contents without calling PagerAdapter#notifyDataSetChanged!" + + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + + " Pager id: " + resName + + " Pager class: " + getClass() + + " Problematic adapter: " + mAdapter.getClass()); + } + + // Locate the currently focused item or add it if needed. + int curIndex = -1; + ItemInfo curItem = null; + for (curIndex = 0; curIndex < mItems.size(); curIndex++) { + final ItemInfo ii = mItems.get(curIndex); + if (ii.position >= mCurItem) { + if (ii.position == mCurItem) curItem = ii; + break; + } + } + + if (curItem == null && N > 0) { + curItem = addNewItem(mCurItem, curIndex); + } + + // Fill 3x the available width or up to the number of offscreen + // pages requested to either side, whichever is larger. + // If we have no current item we have no work to do. + if (curItem != null) { + float extraWidthLeft = 0.f; + int itemIndex = curIndex - 1; + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + final int clientWidth = getClientWidth(); + final float leftWidthNeeded = clientWidth <= 0 ? 0 : + 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; + for (int pos = mCurItem - 1; pos >= 0; pos--) { + if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ii.object); + } + itemIndex--; + curIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthLeft += ii.widthFactor; + itemIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex + 1); + extraWidthLeft += ii.widthFactor; + curIndex++; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } + + float extraWidthRight = curItem.widthFactor; + itemIndex = curIndex + 1; + if (extraWidthRight < 2.f) { + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + final float rightWidthNeeded = clientWidth <= 0 ? 0 : + (float) getPaddingRight() / (float) clientWidth + 2.f; + for (int pos = mCurItem + 1; pos < N; pos++) { + if (extraWidthRight >= rightWidthNeeded && pos > endPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + (ii.object)); + } + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthRight += ii.widthFactor; + itemIndex++; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex); + itemIndex++; + extraWidthRight += ii.widthFactor; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } + } + + calculatePageOffsets(curItem, curIndex, oldCurInfo); + } + + if (DEBUG) { + Log.i(TAG, "Current page list:"); + for (int i=0; i<mItems.size(); i++) { + Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); + } + } + + mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); + + mAdapter.finishUpdate(this); + + // Check width measurement of current pages and drawing sort order. + // Update LayoutParams as needed. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.childIndex = i; + if (!lp.isDecor && lp.widthFactor == 0.f) { + // 0 means requery the adapter for this, it doesn't have a valid width. + final ItemInfo ii = infoForChild(child); + if (ii != null) { + lp.widthFactor = ii.widthFactor; + lp.position = ii.position; + } + } + } + sortChildDrawingOrder(); + + if (hasFocus()) { + View currentFocused = findFocus(); + ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; + if (ii == null || ii.position != mCurItem) { + for (int i=0; i<getChildCount(); i++) { + View child = getChildAt(i); + ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(focusDirection)) { + break; + } + } + } + } + } + } + + private void sortChildDrawingOrder() { + if (mDrawingOrder != DRAW_ORDER_DEFAULT) { + if (mDrawingOrderedChildren == null) { + mDrawingOrderedChildren = new ArrayList<View>(); + } else { + mDrawingOrderedChildren.clear(); + } + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + mDrawingOrderedChildren.add(child); + } + Collections.sort(mDrawingOrderedChildren, sPositionComparator); + } + } + + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { + final int N = mAdapter.getCount(); + final int width = getClientWidth(); + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + // Fix up offsets for later layout. + if (oldCurInfo != null) { + final int oldCurPosition = oldCurInfo.position; + // Base offsets off of oldCurInfo. + if (oldCurPosition < curItem.position) { + int itemIndex = 0; + ItemInfo ii = null; + float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; + for (int pos = oldCurPosition + 1; + pos <= curItem.position && itemIndex < mItems.size(); pos++) { + ii = mItems.get(itemIndex); + while (pos > ii.position && itemIndex < mItems.size() - 1) { + itemIndex++; + ii = mItems.get(itemIndex); + } + while (pos < ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset += mAdapter.getPageWidth(pos) + marginOffset; + pos++; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + } else if (oldCurPosition > curItem.position) { + int itemIndex = mItems.size() - 1; + ItemInfo ii = null; + float offset = oldCurInfo.offset; + for (int pos = oldCurPosition - 1; + pos >= curItem.position && itemIndex >= 0; pos--) { + ii = mItems.get(itemIndex); + while (pos < ii.position && itemIndex > 0) { + itemIndex--; + ii = mItems.get(itemIndex); + } + while (pos > ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset -= mAdapter.getPageWidth(pos) + marginOffset; + pos--; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + } + } + } + + // Base all offsets off of curItem. + final int itemCount = mItems.size(); + float offset = curItem.offset; + int pos = curItem.position - 1; + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; + mLastOffset = curItem.position == N - 1 ? + curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; + // Previous pages + for (int i = curIndex - 1; i >= 0; i--, pos--) { + final ItemInfo ii = mItems.get(i); + while (pos > ii.position) { + offset -= mAdapter.getPageWidth(pos--) + marginOffset; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + if (ii.position == 0) mFirstOffset = offset; + } + offset = curItem.offset + curItem.widthFactor + marginOffset; + pos = curItem.position + 1; + // Next pages + for (int i = curIndex + 1; i < itemCount; i++, pos++) { + final ItemInfo ii = mItems.get(i); + while (pos < ii.position) { + offset += mAdapter.getPageWidth(pos++) + marginOffset; + } + if (ii.position == N - 1) { + mLastOffset = offset + ii.widthFactor - 1; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + + mNeedCalculatePageOffsets = false; + } + + /** + * This is the persistent state that is saved by ViewPager. Only needed + * if you are creating a sublass of ViewPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends BaseSavedState { + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "FragmentPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { + @TargetApi(24) + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }); + + SavedState(Parcel in, ClassLoader loader) { + super(in); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState)state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, false, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + lp.isDecor |= child instanceof Decor; + if (mInLayout) { + if (lp != null && lp.isDecor) { + throw new IllegalStateException("Cannot add pager decor view during layout"); + } + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + + if (USE_CACHE) { + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(mScrollingCacheEnabled); + } else { + child.setDrawingCacheEnabled(false); + } + } + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + ItemInfo infoForChild(View child) { + for (int i=0; i<mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (mAdapter.isViewFromObject(child, ii.object)) { + return ii; + } + } + return null; + } + + ItemInfo infoForAnyChild(View child) { + ViewParent parent; + while ((parent=child.getParent()) != this) { + if (parent == null || !(parent instanceof View)) { + return null; + } + child = (View)parent; + } + return infoForChild(child); + } + + ItemInfo infoForPosition(int position) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.position == position) { + return ii; + } + } + return null; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // For simple implementation, our internal size is always 0. + // We depend on the container to specify the layout size of + // our view. We can't really know what it is since we will be + // adding and removing different arbitrary views and do not + // want the layout to change as this happens. + setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), + getDefaultSize(0, heightMeasureSpec)); + + final int measuredWidth = getMeasuredWidth(); + final int maxGutterSize = measuredWidth / 10; + mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); + + // Children are just made to fill our space. + int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); + int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + /* + * Make sure all children have been properly measured. Decor views first. + * Right now we cheat and make this less complicated by assuming decor + * views won't intersect. We will pin to edges based on gravity. + */ + int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp != null && lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + int widthMode = MeasureSpec.AT_MOST; + int heightMode = MeasureSpec.AT_MOST; + boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; + boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; + + if (consumeVertical) { + widthMode = MeasureSpec.EXACTLY; + } else if (consumeHorizontal) { + heightMode = MeasureSpec.EXACTLY; + } + + int widthSize = childWidthSize; + int heightSize = childHeightSize; + if (lp.width != LayoutParams.WRAP_CONTENT) { + widthMode = MeasureSpec.EXACTLY; + if (lp.width != LayoutParams.FILL_PARENT) { + widthSize = lp.width; + } + } + if (lp.height != LayoutParams.WRAP_CONTENT) { + heightMode = MeasureSpec.EXACTLY; + if (lp.height != LayoutParams.FILL_PARENT) { + heightSize = lp.height; + } + } + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); + final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); + child.measure(widthSpec, heightSpec); + + if (consumeVertical) { + childHeightSize -= child.getMeasuredHeight(); + } else if (consumeHorizontal) { + childWidthSize -= child.getMeasuredWidth(); + } + } + } + } + + mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); + mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); + + // Make sure we have created all fragments that we need to have shown. + mInLayout = true; + populate(); + mInLayout = false; + + // Page views next. + size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child + + ": " + mChildWidthMeasureSpec); + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp == null || !lp.isDecor) { + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); + child.measure(widthSpec, mChildHeightMeasureSpec); + } + } + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // Make sure scroll position is set correctly. + if (w != oldw) { + recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); + } + } + + private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { + if (oldWidth > 0 && !mItems.isEmpty()) { + final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; + final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + + oldMargin; + final int xpos = getScrollX(); + final float pageOffset = (float) xpos / oldWidthWithMargin; + final int newOffsetPixels = (int) (pageOffset * widthWithMargin); + + scrollTo(newOffsetPixels, getScrollY()); + if (!mScroller.isFinished()) { + // We now return to your regularly scheduled scroll, already in progress. + final int newDuration = mScroller.getDuration() - mScroller.timePassed(); + ItemInfo targetInfo = infoForPosition(mCurItem); + mScroller.startScroll(newOffsetPixels, 0, + (int) (targetInfo.offset * width), 0, newDuration); + } + } else { + final ItemInfo ii = infoForPosition(mCurItem); + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; + final int scrollPos = (int) (scrollOffset * + (width - getPaddingLeft() - getPaddingRight())); + if (scrollPos != getScrollX()) { + completeScroll(false); + scrollTo(scrollPos, getScrollY()); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + final int scrollX = getScrollX(); + + int decorCount = 0; + + // First pass - decor views. We need to do this in two passes so that + // we have the proper offsets for non-decor views later. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = 0; + int childTop = 0; + if (lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getMeasuredWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + switch (vgrav) { + default: + childTop = paddingTop; + break; + case Gravity.TOP: + childTop = paddingTop; + paddingTop += child.getMeasuredHeight(); + break; + case Gravity.CENTER_VERTICAL: + childTop = Math.max((height - child.getMeasuredHeight()) / 2, + paddingTop); + break; + case Gravity.BOTTOM: + childTop = height - paddingBottom - child.getMeasuredHeight(); + paddingBottom += child.getMeasuredHeight(); + break; + } + childLeft += scrollX; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + decorCount++; + } + } + } + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo ii; + if (!lp.isDecor && (ii = infoForChild(child)) != null) { + int loff = (int) (childWidth * ii.offset); + int childLeft = paddingLeft + loff; + int childTop = paddingTop; + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidth * lp.widthFactor), + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + (int) (height - paddingTop - paddingBottom), + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + int id = -1; + + if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + + "x" + child.getMeasuredHeight()); + + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + } + mTopPageBounds = paddingTop; + mBottomPageBounds = height - paddingBottom; + mDecorChildCount = decorCount; + + if (mFirstLayout) { + scrollToItem(mCurItem, false, 0, false); + } + mFirstLayout = false; + } + + @Override + public void computeScroll() { + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (!pageScrolled(x)) { + mScroller.abortAnimation(); + scrollTo(0, y); + } + } + + // Keep on drawing until the animation has finished. + ViewCompat.postInvalidateOnAnimation(this); + return; + } + + // Done with scroll, clean up state. + completeScroll(true); + } + + private boolean pageScrolled(int xpos) { + if (mItems.size() == 0) { + mCalledSuper = false; + onPageScrolled(0, 0, 0); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return false; + } + final ItemInfo ii = infoForCurrentScrollPosition(); + final int width = getClientWidth(); + final int widthWithMargin = width + mPageMargin; + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) xpos / width) - ii.offset) / + (ii.widthFactor + marginOffset); + final int offsetPixels = (int) (pageOffset * widthWithMargin); + + mCalledSuper = false; + onPageScrolled(currentPage, pageOffset, offsetPixels); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return true; + } + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * If you override this method you must call through to the superclass implementation + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled + * returns. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param offset Value from [0, 1) indicating the offset from the page at position. + * @param offsetPixels Value in pixels indicating the offset from position. + */ + protected void onPageScrolled(int position, float offset, int offsetPixels) { + // Offset any decor views if needed - keep them on-screen at all times. + if (mDecorChildCount > 0) { + final int scrollX = getScrollX(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + final int width = getWidth(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) continue; + + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int childLeft = 0; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + childLeft += scrollX; + + final int childOffset = childLeft - child.getLeft(); + if (childOffset != 0) { + child.offsetLeftAndRight(childOffset); + } + } + } + + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + + if (mPageTransformer != null) { + final int scrollX = getScrollX(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.isDecor) continue; + + final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); + mPageTransformer.transformPage(child, transformPos); + } + } + + mCalledSuper = true; + } + + private void completeScroll(boolean postEvents) { + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; + if (needPopulate) { + // Done with scroll, no longer want to cache view drawing. + setScrollingCacheEnabled(false); + mScroller.abortAnimation(); + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (oldX != x || oldY != y) { + scrollTo(x, y); + } + } + mPopulatePending = false; + for (int i=0; i<mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.scrolling) { + needPopulate = true; + ii.scrolling = false; + } + } + if (needPopulate) { + if (postEvents) { + ViewCompat.postOnAnimation(this, mEndScrollRunnable); + } else { + mEndScrollRunnable.run(); + } + } + } + + private boolean isGutterDrag(float x, float dx) { + return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0); + } + + private void enableLayers(boolean enable) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final int layerType = enable ? + ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; + ViewCompat.setLayerType(getChildAt(i), layerType, null); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + + // Always take care of the touch gesture being complete. + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Release the drag. + if (DEBUG) Log.v(TAG, "Intercept done!"); + mIsBeingDragged = false; + mIsUnableToDrag = false; + mActivePointerId = INVALID_POINTER; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + return false; + } + + // Nothing more to do here if we have decided whether or not we + // are dragging. + if (action != MotionEvent.ACTION_DOWN) { + if (mIsBeingDragged) { + if (DEBUG) Log.v(TAG, "Intercept returning true!"); + return true; + } + if (mIsUnableToDrag) { + if (DEBUG) Log.v(TAG, "Intercept returning false!"); + return false; + } + } + + switch (action) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); + final float x = MotionEventCompat.getX(ev, pointerIndex); + final float dx = x - mLastMotionX; + final float xDiff = Math.abs(dx); + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float yDiff = Math.abs(y - mInitialMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + + if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && + canScroll(this, false, (int) dx, (int) x, (int) y)) { + // Nested view has scrollable area under this point. Let it be handled there. + mLastMotionX = x; + mLastMotionY = y; + mIsUnableToDrag = true; + return false; + } + if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollingCacheEnabled(true); + } else if (yDiff > mTouchSlop) { + // The finger has moved enough in the vertical + // direction to be counted as a drag... abort + // any attempt to drag horizontally, to work correctly + // with children that have scrolling containers. + if (DEBUG) Log.v(TAG, "Starting unable to drag!"); + mIsUnableToDrag = true; + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + if (performDrag(x)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsUnableToDrag = false; + + mScroller.computeScrollOffset(); + if (mScrollState == SCROLL_STATE_SETTLING && + Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { + // Let the user 'catch' the pager as it animates. + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } else { + completeScroll(false); + mIsBeingDragged = false; + } + + if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + + " mIsBeingDragged=" + mIsBeingDragged + + "mIsUnableToDrag=" + mIsUnableToDrag); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mFakeDragging) { + // A fake drag is in progress already, ignore this real one + // but still eat the touch events. + // (It is likely that the user is multi-touching the screen.) + return true; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (mAdapter == null || mAdapter.getCount() == 0) { + // Nothing to present or scroll; nothing to touch. + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + boolean needsInvalidate = false; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + + // Remember where the motion event started + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + break; + } + case MotionEvent.ACTION_MOVE: + if (!mIsBeingDragged) { + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, pointerIndex); + final float xDiff = Math.abs(x - mLastMotionX); + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float yDiff = Math.abs(y - mLastMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + if (xDiff > mTouchSlop && xDiff > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollState(SCROLL_STATE_DRAGGING); + setScrollingCacheEnabled(true); + + // Disallow Parent Intercept, just in case + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + // Not else! Note that mIsBeingDragged can be set above. + if (mIsBeingDragged) { + // Scroll to follow the motion event + final int activePointerIndex = MotionEventCompat.findPointerIndex( + ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + needsInvalidate |= performDrag(x); + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( + velocityTracker, mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int activePointerIndex = + MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final int totalDelta = (int) (x - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + + mActivePointerId = INVALID_POINTER; + endDrag(); + needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged) { + scrollToItem(mCurItem, true, 0, false); + mActivePointerId = INVALID_POINTER; + endDrag(); + needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); + } + break; + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + final float x = MotionEventCompat.getX(ev, index); + mLastMotionX = x; + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = MotionEventCompat.getX(ev, + MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + return true; + } + + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + private boolean performDrag(float x) { + boolean needsInvalidate = false; + + final float deltaX = mLastMotionX - x; + mLastMotionX = x; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX + deltaX; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + boolean leftAbsolute = true; + boolean rightAbsolute = true; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftAbsolute = false; + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightAbsolute = false; + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + if (leftAbsolute) { + float over = leftBound - scrollX; + needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); + } + scrollX = leftBound; + } else if (scrollX > rightBound) { + if (rightAbsolute) { + float over = scrollX - rightBound; + needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); + } + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + return needsInvalidate; + } + + /** + * @return Info about the page at the current scroll position. + * This can be synthetic for a missing middle page; the 'object' field can be null. + */ + private ItemInfo infoForCurrentScrollPosition() { + final int width = getClientWidth(); + final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + int lastPos = -1; + float lastOffset = 0.f; + float lastWidth = 0.f; + boolean first = true; + + ItemInfo lastItem = null; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + float offset; + if (!first && ii.position != lastPos + 1) { + // Create a synthetic item for a missing page. + ii = mTempItem; + ii.offset = lastOffset + lastWidth + marginOffset; + ii.position = lastPos + 1; + ii.widthFactor = mAdapter.getPageWidth(ii.position); + i--; + } + offset = ii.offset; + + final float leftBound = offset; + final float rightBound = offset + ii.widthFactor + marginOffset; + if (first || scrollOffset >= leftBound) { + if (scrollOffset < rightBound || i == mItems.size() - 1) { + return ii; + } + } else { + return lastItem; + } + first = false; + lastPos = ii.position; + lastOffset = offset; + lastWidth = ii.widthFactor; + lastItem = ii; + } + + return lastItem; + } + + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { + int targetPage; + if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { + targetPage = velocity > 0 ? currentPage : currentPage + 1; + } else { + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; + targetPage = (int) (currentPage + pageOffset + truncator); + } + + if (mItems.size() > 0) { + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + + // Only let the user target pages we have items for + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); + } + + return targetPage; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + boolean needsInvalidate = false; + + final int overScrollMode = ViewCompat.getOverScrollMode(this); + if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && + mAdapter != null && mAdapter.getCount() > 1)) { + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + final int width = getWidth(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), mFirstOffset * width); + mLeftEdge.setSize(height, width); + needsInvalidate |= mLeftEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); + mRightEdge.setSize(height, width); + needsInvalidate |= mRightEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + } else { + mLeftEdge.finish(); + mRightEdge.finish(); + } + + if (needsInvalidate) { + // Keep animating + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the margin drawable between pages if needed. + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { + final int scrollX = getScrollX(); + final int width = getWidth(); + + final float marginOffset = (float) mPageMargin / width; + int itemIndex = 0; + ItemInfo ii = mItems.get(0); + float offset = ii.offset; + final int itemCount = mItems.size(); + final int firstPos = ii.position; + final int lastPos = mItems.get(itemCount - 1).position; + for (int pos = firstPos; pos < lastPos; pos++) { + while (pos > ii.position && itemIndex < itemCount) { + ii = mItems.get(++itemIndex); + } + + float drawAt; + if (pos == ii.position) { + drawAt = (ii.offset + ii.widthFactor) * width; + offset = ii.offset + ii.widthFactor + marginOffset; + } else { + float widthFactor = mAdapter.getPageWidth(pos); + drawAt = (offset + widthFactor) * width; + offset += widthFactor + marginOffset; + } + + if (drawAt + mPageMargin > scrollX) { + mMarginDrawable.setBounds((int) drawAt, mTopPageBounds, + (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); + mMarginDrawable.draw(canvas); + } + + if (drawAt > scrollX + width) { + break; // No more visible, no sense in continuing + } + } + } + } + + /** + * Start a fake drag of the pager. + * + * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager + * with the touch scrolling of another view, while still letting the ViewPager + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. + * + * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag + * is already in progress, this method will return false. + * + * @return true if the fake drag began successfully, false if it could not be started. + * + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean beginFakeDrag() { + if (mIsBeingDragged) { + return false; + } + mFakeDragging = true; + setScrollState(SCROLL_STATE_DRAGGING); + mInitialMotionX = mLastMotionX = 0; + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + mFakeDragBeginTime = time; + return true; + } + + /** + * End a fake drag of the pager. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + */ + public void endFakeDrag() { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( + velocityTracker, mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int totalDelta = (int) (mLastMotionX - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + endDrag(); + + mFakeDragging = false; + } + + /** + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. + * + * @param xOffset Offset in pixels to drag by. + * @see #beginFakeDrag() + * @see #endFakeDrag() + */ + public void fakeDragBy(float xOffset) { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + mLastMotionX += xOffset; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX - xOffset; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + scrollX = leftBound; + } else if (scrollX > rightBound) { + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + // Synthesize an event for the VelocityTracker. + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, + mLastMotionX, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + } + + /** + * Returns true if a fake drag is in progress. + * + * @return true if currently in a fake drag, false otherwise. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean isFakeDragging() { + return mFakeDragging; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + mIsUnableToDrag = false; + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled != enabled) { + mScrollingCacheEnabled = enabled; + if (USE_CACHE) { + final int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(enabled); + } + } + } + } + } + + public boolean canScrollHorizontally(int direction) { + if (mAdapter == null) { + return false; + } + + final int width = getClientWidth(); + final int scrollX = getScrollX(); + if (direction < 0) { + return (scrollX > (int) (width * mFirstOffset)); + } else if (direction > 0) { + return (scrollX < (int) (width * mLastOffset)); + } else { + return false; + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && ViewCompat.canScrollHorizontally(v, -dx); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = arrowScroll(FOCUS_LEFT); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = arrowScroll(FOCUS_RIGHT); + break; + } + } + return handled; + } + + public boolean arrowScroll(int direction) { + View currentFocused = findFocus(); + if (currentFocused == this) { + currentFocused = null; + } else if (currentFocused != null) { + boolean isChild = false; + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + if (parent == this) { + isChild = true; + break; + } + } + if (!isChild) { + // This would cause the focus search down below to fail in fun ways. + final StringBuilder sb = new StringBuilder(); + sb.append(currentFocused.getClass().getSimpleName()); + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + sb.append(" => ").append(parent.getClass().getSimpleName()); + } + Log.e(TAG, "arrowScroll tried to find focus based on non-child " + + "current focused view " + sb.toString()); + currentFocused = null; + } + } + + boolean handled = false; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, + direction); + if (nextFocused != null && nextFocused != currentFocused) { + if (direction == View.FOCUS_LEFT) { + // If there is nothing to the left, or this is causing us to + // jump to the right, then what we really want to do is page left. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft >= currLeft) { + handled = pageLeft(); + } else { + handled = nextFocused.requestFocus(); + } + } else if (direction == View.FOCUS_RIGHT) { + // If there is nothing to the right, or this is causing us to + // jump to the left, then what we really want to do is page right. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft <= currLeft) { + handled = pageRight(); + } else { + handled = nextFocused.requestFocus(); + } + } + } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { + // Trying to move left and nothing there; try to page. + handled = pageLeft(); + } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { + // Trying to move right and nothing there; try to page. + handled = pageRight(); + } + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } + + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { + if (outRect == null) { + outRect = new Rect(); + } + if (child == null) { + outRect.set(0, 0, 0, 0); + return outRect; + } + outRect.left = child.getLeft(); + outRect.right = child.getRight(); + outRect.top = child.getTop(); + outRect.bottom = child.getBottom(); + + ViewParent parent = child.getParent(); + while (parent instanceof ViewGroup && parent != this) { + final ViewGroup group = (ViewGroup) parent; + outRect.left += group.getLeft(); + outRect.right += group.getRight(); + outRect.top += group.getTop(); + outRect.bottom += group.getBottom(); + + parent = group.getParent(); + } + return outRect; + } + + boolean pageLeft() { + if (mCurItem > 0) { + setCurrentItem(mCurItem-1, true); + return true; + } + return false; + } + + boolean pageRight() { + if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { + setCurrentItem(mCurItem+1, true); + return true; + } + return false; + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + final int focusableCount = views.size(); + + final int descendantFocusability = getDescendantFocusability(); + + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addFocusables(views, direction, focusableMode); + } + } + } + } + + // we add ourselves (if focusable) in all cases except for when we are + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is + // to avoid the focus search finding layouts when a more precise search + // among the focusable children would be more interesting. + if ( + descendantFocusability != FOCUS_AFTER_DESCENDANTS || + // No focusable descendants + (focusableCount == views.size())) { + // Note that we can't call the superclass here, because it will + // add all views in. So we need to do the same thing View does. + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && + isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + if (views != null) { + views.add(this); + } + } + } + + /** + * We only want the current page that is being shown to be touchable. + */ + @Override + public void addTouchables(ArrayList<View> views) { + // Note that we don't call super.addTouchables(), which means that + // we don't call View.addTouchables(). This is okay because a ViewPager + // is itself not touchable. + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addTouchables(views); + } + } + } + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + int index; + int increment; + int end; + int count = getChildCount(); + if ((direction & FOCUS_FORWARD) != 0) { + index = 0; + increment = 1; + end = count; + } else { + index = count - 1; + increment = -1; + end = -1; + } + for (int i = index; i != end; i += increment) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Dispatch scroll events from this ViewPager. + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + return super.dispatchPopulateAccessibilityEvent(event); + } + + // Dispatch all other accessibility events from the current page. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + final ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem && + child.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + } + + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + class MyAccessibilityDelegate extends AccessibilityDelegateCompat { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(ViewPager.class.getName()); + final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain(); + recordCompat.setScrollable(canScroll()); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED + && mAdapter != null) { + recordCompat.setItemCount(mAdapter.getCount()); + recordCompat.setFromIndex(mCurItem); + recordCompat.setToIndex(mCurItem); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(ViewPager.class.getName()); + info.setScrollable(canScroll()); + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + } return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + } return false; + } + return false; + } + + private boolean canScroll() { + return (mAdapter != null) && (mAdapter.getCount() > 1); + } + } + + private class PagerObserver extends DataSetObserver { + @Override + public void onChanged() { + dataSetChanged(); + } + @Override + public void onInvalidated() { + dataSetChanged(); + } + } + + /** + * Layout parameters that should be supplied for views added to a + * ViewPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if this view is a decoration on the pager itself and not + * a view supplied by the adapter. + */ + public boolean isDecor; + + /** + * Gravity setting for use on decor views only: + * Where to position the view page within the overall ViewPager + * container; constants are defined in {@link android.view.Gravity}. + */ + public int gravity; + + /** + * Width as a 0-1 multiplier of the measured pager width + */ + float widthFactor = 0.f; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the ViewPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(FILL_PARENT, FILL_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + gravity = a.getInteger(0, Gravity.TOP); + a.recycle(); + } + } + + static class ViewPositionComparator implements Comparator<View> { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); + if (llp.isDecor != rlp.isDecor) { + return llp.isDecor ? 1 : -1; + } + return llp.position - rlp.position; + } + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java b/main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java new file mode 100644 index 00000000..ce14cc98 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Build; +import android.os.IBinder; +import android.os.RemoteException; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import android.widget.Toast; + +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.IOpenVPNServiceInternal; +import de.blinkt.openvpn.core.OpenVPNService; +import de.blinkt.openvpn.core.ProfileManager; +import de.blinkt.openvpn.core.VpnStatus; + + +/** + * Created by arne on 22.04.16. + */ +@TargetApi(Build.VERSION_CODES.N) +public class OpenVPNTileService extends TileService implements VpnStatus.StateListener { + + @SuppressLint("Override") + @TargetApi(Build.VERSION_CODES.N) + @Override + public void onClick() { + super.onClick(); + final VpnProfile bootProfile = getQSVPN(); + if (bootProfile == null) { + Toast.makeText(this, R.string.novpn_selected, Toast.LENGTH_SHORT).show(); + } else { + if (!isLocked()) + clickAction(bootProfile); + else + unlockAndRun(new Runnable() { + @Override + public void run() { + clickAction(bootProfile); + } + }); + } + } + + private void clickAction(VpnProfile bootProfile) { + if (VpnStatus.isVPNActive()) { + Intent intent = new Intent(this, OpenVPNService.class); + intent.setAction(OpenVPNService.START_SERVICE); + bindService(intent, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder binder) { + IOpenVPNServiceInternal service = IOpenVPNServiceInternal.Stub.asInterface(binder); + + if (service != null) + try { + service.stopVPN(false); + } catch (RemoteException e) { + VpnStatus.logException(e); + } + + unbindService(this); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + + } + }, Context.BIND_AUTO_CREATE); + } else + launchVPN(bootProfile, this); + } + + + @SuppressLint("Override") + @TargetApi(Build.VERSION_CODES.N) + void launchVPN(VpnProfile profile, Context context) { + Intent startVpnIntent = new Intent(Intent.ACTION_MAIN); + startVpnIntent.setClass(context, LaunchVPN.class); + startVpnIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUIDString()); + startVpnIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startVpnIntent.putExtra(LaunchVPN.EXTRA_HIDELOG, true); + + context.startActivity(startVpnIntent); + } + + @TargetApi(Build.VERSION_CODES.N) + @Override + public void onTileAdded() { + } + + @Override + public void onStartListening() { + super.onStartListening(); + VpnStatus.addStateListener(this); + } + + + @TargetApi(Build.VERSION_CODES.N) + public VpnProfile getQSVPN() { + return ProfileManager.getAlwaysOnVPN(this); + } + + @Override + public void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level) { + VpnProfile vpn; + Tile t = getQsTile(); + if (level == ConnectionStatus.LEVEL_AUTH_FAILED || level == ConnectionStatus.LEVEL_NOTCONNECTED) { + // No VPN connected, use stadnard VPN + vpn = getQSVPN(); + if (vpn == null) { + t.setLabel(getString(R.string.novpn_selected)); + t.setState(Tile.STATE_UNAVAILABLE); + } else { + t.setLabel(getString(R.string.qs_connect, vpn.getName())); + t.setState(Tile.STATE_INACTIVE); + } + } else { + vpn = ProfileManager.get(getBaseContext(), VpnStatus.getLastConnectedVPNProfile()); + String name; + if (vpn == null) + name = "null?!"; + else + name = vpn.getName(); + t.setLabel(getString(R.string.qs_disconnect, name)); + t.setState(Tile.STATE_ACTIVE); + } + + t.updateTile(); + } + + @Override + public void setConnectedVPN(String uuid) { + + } + + @Override + public void onStopListening() { + VpnStatus.removeStateListener(this); + super.onStopListening(); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java b/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java new file mode 100644 index 00000000..7258d8d6 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012-2015 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.activities; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.UiModeManager; +import android.content.Context; +import android.content.RestrictionsManager; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.os.UserManager; +import android.view.Window; +import de.blinkt.openvpn.api.AppRestrictions; + +public class BaseActivity extends Activity { + private boolean isAndroidTV() { + final UiModeManager uiModeManager = (UiModeManager) getSystemService(Activity.UI_MODE_SERVICE); + return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (isAndroidTV()) { + requestWindowFeature(Window.FEATURE_OPTIONS_PANEL); + } + super.onCreate(savedInstanceState); + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java b/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java new file mode 100644 index 00000000..38b47b5a --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java @@ -0,0 +1,847 @@ + +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.activities; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.OpenableColumns; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Pair; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ConfigParser; +import de.blinkt.openvpn.core.ConfigParser.ConfigParseError; +import de.blinkt.openvpn.core.ProfileManager; +import de.blinkt.openvpn.fragments.Utils; +import de.blinkt.openvpn.views.FileSelectLayout; + +import static de.blinkt.openvpn.views.FileSelectLayout.FileSelectCallback; + +public class ConfigConverter extends BaseActivity implements FileSelectCallback, View.OnClickListener { + + public static final String IMPORT_PROFILE = "de.blinkt.openvpn.IMPORT_PROFILE"; + private static final int RESULT_INSTALLPKCS12 = 7; + private static final int CHOOSE_FILE_OFFSET = 1000; + public static final String VPNPROFILE = "vpnProfile"; + private static final int PERMISSION_REQUEST_EMBED_FILES = 37231; + private static final int PERMISSION_REQUEST_READ_URL = PERMISSION_REQUEST_EMBED_FILES + 1; + + private VpnProfile mResult; + + private transient List<String> mPathsegments; + + private String mAliasName = null; + + + private Map<Utils.FileType, FileSelectLayout> fileSelectMap = new HashMap<>(); + private String mEmbeddedPwFile; + private Vector<String> mLogEntries = new Vector<>(); + private Uri mSourceUri; + private EditText mProfilename; + private AsyncTask<Void, Void, Integer> mImportTask; + private LinearLayout mLogLayout; + private TextView mProfilenameLabel; + + @Override + public void onClick(View v) { + if (v.getId() == R.id.fab_save) + userActionSaveProfile(); + if (v.getId() == R.id.permssion_hint && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) + doRequestSDCardPermission(PERMISSION_REQUEST_EMBED_FILES); + + } + + @TargetApi(Build.VERSION_CODES.M) + private void doRequestSDCardPermission(int requestCode) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, requestCode); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + // Permission declined, do nothing + if (grantResults.length == 0 || grantResults[0] == PackageManager.PERMISSION_DENIED) + return; + + // Reset file select dialogs + findViewById(R.id.files_missing_hint).setVisibility(View.GONE); + findViewById(R.id.permssion_hint).setVisibility(View.GONE); + LinearLayout fileroot = (LinearLayout) findViewById(R.id.config_convert_root); + for (int i = 0; i < fileroot.getChildCount(); ) { + if (fileroot.getChildAt(i) instanceof FileSelectLayout) + fileroot.removeViewAt(i); + else + i++; + } + + if (requestCode == PERMISSION_REQUEST_EMBED_FILES) + embedFiles(null); + + else if (requestCode == PERMISSION_REQUEST_READ_URL) { + if (mSourceUri != null) + doImportUri(mSourceUri); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.cancel) { + setResult(Activity.RESULT_CANCELED); + finish(); + } else if (item.getItemId() == R.id.ok) { + return userActionSaveProfile(); + } + + return super.onOptionsItemSelected(item); + + } + + private boolean userActionSaveProfile() { + if (mResult == null) { + log(R.string.import_config_error); + Toast.makeText(this, R.string.import_config_error, Toast.LENGTH_LONG).show(); + return true; + } + + mResult.mName = mProfilename.getText().toString(); + ProfileManager vpl = ProfileManager.getInstance(this); + if (vpl.getProfileByName(mResult.mName) != null) { + mProfilename.setError(getString(R.string.duplicate_profile_name)); + return true; + } + + Intent in = installPKCS12(); + + if (in != null) + startActivityForResult(in, RESULT_INSTALLPKCS12); + else + saveProfile(); + + return true; + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (mResult != null) + outState.putSerializable(VPNPROFILE, mResult); + outState.putString("mAliasName", mAliasName); + + + String[] logentries = mLogEntries.toArray(new String[mLogEntries.size()]); + + outState.putStringArray("logentries", logentries); + + int[] fileselects = new int[fileSelectMap.size()]; + int k = 0; + for (Utils.FileType key : fileSelectMap.keySet()) { + fileselects[k] = key.getValue(); + k++; + } + outState.putIntArray("fileselects", fileselects); + outState.putString("pwfile", mEmbeddedPwFile); + outState.putParcelable("mSourceUri", mSourceUri); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent result) { + if (requestCode == RESULT_INSTALLPKCS12 && resultCode == Activity.RESULT_OK) { + showCertDialog(); + } + + if (resultCode == Activity.RESULT_OK && requestCode >= CHOOSE_FILE_OFFSET) { + Utils.FileType type = Utils.FileType.getFileTypeByValue(requestCode - CHOOSE_FILE_OFFSET); + + + FileSelectLayout fs = fileSelectMap.get(type); + fs.parseResponse(result, this); + + String data = fs.getData(); + + switch (type) { + case USERPW_FILE: + mEmbeddedPwFile = data; + break; + case PKCS12: + mResult.mPKCS12Filename = data; + break; + case TLS_AUTH_FILE: + mResult.mTLSAuthFilename = data; + break; + case CA_CERTIFICATE: + mResult.mCaFilename = data; + break; + case CLIENT_CERTIFICATE: + mResult.mClientCertFilename = data; + break; + case KEYFILE: + mResult.mClientKeyFilename = data; + break; + case CRL_FILE: + mResult.mCrlFilename = data; + break; + default: + throw new RuntimeException("Type is wrong somehow?"); + } + } + + super.onActivityResult(requestCode, resultCode, result); + } + + private void saveProfile() { + Intent result = new Intent(); + ProfileManager vpl = ProfileManager.getInstance(this); + + if (!TextUtils.isEmpty(mEmbeddedPwFile)) + ConfigParser.useEmbbedUserAuth(mResult, mEmbeddedPwFile); + + vpl.addProfile(mResult); + vpl.saveProfile(this, mResult); + vpl.saveProfileList(this); + result.putExtra(VpnProfile.EXTRA_PROFILEUUID, mResult.getUUID().toString()); + setResult(Activity.RESULT_OK, result); + finish(); + } + + public void showCertDialog() { + try { + //noinspection WrongConstant + KeyChain.choosePrivateKeyAlias(this, + new KeyChainAliasCallback() { + + public void alias(String alias) { + // Credential alias selected. Remember the alias selection for future use. + mResult.mAlias = alias; + saveProfile(); + } + + + }, + new String[]{"RSA", "EC"}, // List of acceptable key types. null for any + null, // issuer, null for any + mResult.mServerName, // host name of server requesting the cert, null if unavailable + -1, // port of server requesting the cert, -1 if unavailable + mAliasName); // alias to preselect, null if unavailable + } catch (ActivityNotFoundException anf) { + Builder ab = new AlertDialog.Builder(this); + ab.setTitle(R.string.broken_image_cert_title); + ab.setMessage(R.string.broken_image_cert); + ab.setPositiveButton(android.R.string.ok, null); + ab.show(); + } + } + + + private Intent installPKCS12() { + + if (!((CheckBox) findViewById(R.id.importpkcs12)).isChecked()) { + setAuthTypeToEmbeddedPKCS12(); + return null; + + } + String pkcs12datastr = mResult.mPKCS12Filename; + if (VpnProfile.isEmbedded(pkcs12datastr)) { + Intent inkeyIntent = KeyChain.createInstallIntent(); + + pkcs12datastr = VpnProfile.getEmbeddedContent(pkcs12datastr); + + + byte[] pkcs12data = Base64.decode(pkcs12datastr, Base64.DEFAULT); + + + inkeyIntent.putExtra(KeyChain.EXTRA_PKCS12, pkcs12data); + + if (mAliasName.equals("")) + mAliasName = null; + + if (mAliasName != null) { + inkeyIntent.putExtra(KeyChain.EXTRA_NAME, mAliasName); + } + return inkeyIntent; + + } + return null; + } + + + private void setAuthTypeToEmbeddedPKCS12() { + if (VpnProfile.isEmbedded(mResult.mPKCS12Filename)) { + if (mResult.mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) + mResult.mAuthenticationType = VpnProfile.TYPE_USERPASS_PKCS12; + + if (mResult.mAuthenticationType == VpnProfile.TYPE_KEYSTORE) + mResult.mAuthenticationType = VpnProfile.TYPE_PKCS12; + + } + } + + + private String getUniqueProfileName(String possibleName) { + + int i = 0; + + ProfileManager vpl = ProfileManager.getInstance(this); + + String newname = possibleName; + + // Default to + if (mResult.mName != null && !ConfigParser.CONVERTED_PROFILE.equals(mResult.mName)) + newname = mResult.mName; + + while (newname == null || vpl.getProfileByName(newname) != null) { + i++; + if (i == 1) + newname = getString(R.string.converted_profile); + else + newname = getString(R.string.converted_profile_i, i); + } + + return newname; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.import_menu, menu); + return true; + } + + private String embedFile(String filename, Utils.FileType type, boolean onlyFindFileAndNullonNotFound) { + if (filename == null) + return null; + + // Already embedded, nothing to do + if (VpnProfile.isEmbedded(filename)) + return filename; + + File possibleFile = findFile(filename, type); + if (possibleFile == null) + if (onlyFindFileAndNullonNotFound) + return null; + else + return filename; + else if (onlyFindFileAndNullonNotFound) + return possibleFile.getAbsolutePath(); + else + return readFileContent(possibleFile, type == Utils.FileType.PKCS12); + + } + + + private Pair<Integer, String> getFileDialogInfo(Utils.FileType type) { + int titleRes = 0; + String value = null; + switch (type) { + case KEYFILE: + titleRes = R.string.client_key_title; + if (mResult != null) + value = mResult.mClientKeyFilename; + break; + case CLIENT_CERTIFICATE: + titleRes = R.string.client_certificate_title; + if (mResult != null) + value = mResult.mClientCertFilename; + break; + case CA_CERTIFICATE: + titleRes = R.string.ca_title; + if (mResult != null) + value = mResult.mCaFilename; + break; + case TLS_AUTH_FILE: + titleRes = R.string.tls_auth_file; + if (mResult != null) + value = mResult.mTLSAuthFilename; + break; + case PKCS12: + titleRes = R.string.client_pkcs12_title; + if (mResult != null) + value = mResult.mPKCS12Filename; + break; + + case USERPW_FILE: + titleRes = R.string.userpw_file; + value = mEmbeddedPwFile; + break; + + case CRL_FILE: + titleRes = R.string.crl_file; + value = mResult.mCrlFilename; + break; + } + + return Pair.create(titleRes, value); + + } + + private File findFile(String filename, Utils.FileType fileType) { + File foundfile = findFileRaw(filename); + + if (foundfile == null && filename != null && !filename.equals("")) { + log(R.string.import_could_not_open, filename); + } + fileSelectMap.put(fileType, null); + + return foundfile; + } + + private void addMissingFileDialogs() + { + for (Map.Entry<Utils.FileType, FileSelectLayout> item: fileSelectMap.entrySet()) { + if (item.getValue()==null) + addFileSelectDialog(item.getKey()); + } + } + + private void addFileSelectDialog(Utils.FileType type) { + + Pair<Integer, String> fileDialogInfo = getFileDialogInfo(type); + + boolean isCert = type == Utils.FileType.CA_CERTIFICATE || type == Utils.FileType.CLIENT_CERTIFICATE; + FileSelectLayout fl = new FileSelectLayout(this, getString(fileDialogInfo.first), isCert, false); + fileSelectMap.put(type, fl); + fl.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + ((LinearLayout) findViewById(R.id.config_convert_root)).addView(fl, 2); + findViewById(R.id.files_missing_hint).setVisibility(View.VISIBLE); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) + checkPermission(); + + fl.setData(fileDialogInfo.second, this); + int i = getFileLayoutOffset(type); + fl.setCaller(this, i, type); + + } + + @TargetApi(Build.VERSION_CODES.M) + private void checkPermission() { + if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + findViewById(R.id.permssion_hint).setVisibility(View.VISIBLE); + findViewById(R.id.permssion_hint).setOnClickListener(this); + } + } + + private int getFileLayoutOffset(Utils.FileType type) { + return CHOOSE_FILE_OFFSET + type.getValue(); + } + + + private File findFileRaw(String filename) { + if (filename == null || filename.equals("")) + return null; + + // Try diffent path relative to /mnt/sdcard + File sdcard = Environment.getExternalStorageDirectory(); + File root = new File("/"); + + HashSet<File> dirlist = new HashSet<>(); + + for (int i = mPathsegments.size() - 1; i >= 0; i--) { + String path = ""; + for (int j = 0; j <= i; j++) { + path += "/" + mPathsegments.get(j); + } + // Do a little hackish dance for the Android File Importer + // /document/primary:ovpn/openvpn-imt.conf + + + if (path.indexOf(':') != -1 && path.lastIndexOf('/') > path.indexOf(':')) { + String possibleDir = path.substring(path.indexOf(':') + 1, path.length()); + // Unquote chars in the path + try { + possibleDir = URLDecoder.decode(possibleDir, "UTF-8"); + } catch (UnsupportedEncodingException ignored) {} + + possibleDir = possibleDir.substring(0, possibleDir.lastIndexOf('/')); + + + + + dirlist.add(new File(sdcard, possibleDir)); + + } + dirlist.add(new File(path)); + + + } + dirlist.add(sdcard); + dirlist.add(root); + + + String[] fileparts = filename.split("/"); + for (File rootdir : dirlist) { + String suffix = ""; + for (int i = fileparts.length - 1; i >= 0; i--) { + if (i == fileparts.length - 1) + suffix = fileparts[i]; + else + suffix = fileparts[i] + "/" + suffix; + + File possibleFile = new File(rootdir, suffix); + if (possibleFile.canRead()) + return possibleFile; + + } + } + return null; + } + + String readFileContent(File possibleFile, boolean base64encode) { + byte[] filedata; + try { + filedata = readBytesFromFile(possibleFile); + } catch (IOException e) { + log(e.getLocalizedMessage()); + return null; + } + + String data; + if (base64encode) { + data = Base64.encodeToString(filedata, Base64.DEFAULT); + } else { + data = new String(filedata); + + } + + return VpnProfile.DISPLAYNAME_TAG + possibleFile.getName() + VpnProfile.INLINE_TAG + data; + + } + + + private byte[] readBytesFromFile(File file) throws IOException { + InputStream input = new FileInputStream(file); + + long len = file.length(); + if (len > VpnProfile.MAX_EMBED_FILE_SIZE) + throw new IOException("File size of file to import too large."); + + // Create the byte array to hold the data + byte[] bytes = new byte[(int) len]; + + // Read in the bytes + int offset = 0; + int bytesRead; + while (offset < bytes.length + && (bytesRead = input.read(bytes, offset, bytes.length - offset)) >= 0) { + offset += bytesRead; + } + + input.close(); + return bytes; + } + + void embedFiles(ConfigParser cp) { + // This where I would like to have a c++ style + // void embedFile(std::string & option) + + if (mResult.mPKCS12Filename != null) { + File pkcs12file = findFileRaw(mResult.mPKCS12Filename); + if (pkcs12file != null) { + mAliasName = pkcs12file.getName().replace(".p12", ""); + } else { + mAliasName = "Imported PKCS12"; + } + } + + + mResult.mCaFilename = embedFile(mResult.mCaFilename, Utils.FileType.CA_CERTIFICATE, false); + mResult.mClientCertFilename = embedFile(mResult.mClientCertFilename, Utils.FileType.CLIENT_CERTIFICATE, false); + mResult.mClientKeyFilename = embedFile(mResult.mClientKeyFilename, Utils.FileType.KEYFILE, false); + mResult.mTLSAuthFilename = embedFile(mResult.mTLSAuthFilename, Utils.FileType.TLS_AUTH_FILE, false); + mResult.mPKCS12Filename = embedFile(mResult.mPKCS12Filename, Utils.FileType.PKCS12, false); + mResult.mCrlFilename = embedFile(mResult.mCrlFilename, Utils.FileType.CRL_FILE, true); + if (cp != null) { + mEmbeddedPwFile = cp.getAuthUserPassFile(); + mEmbeddedPwFile = embedFile(cp.getAuthUserPassFile(), Utils.FileType.USERPW_FILE, false); + } + + } + + private void updateFileSelectDialogs() { + for (Map.Entry<Utils.FileType, FileSelectLayout> fl : fileSelectMap.entrySet()) { + fl.getValue().setData(getFileDialogInfo(fl.getKey()).second, this); + } + } + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.config_converter); + + ImageButton fab_button = (ImageButton) findViewById(R.id.fab_save); + if (fab_button != null) { + fab_button.setOnClickListener(this); + findViewById(R.id.fab_footerspace).setVisibility(View.VISIBLE); + } + + mLogLayout = (LinearLayout) findViewById(R.id.config_convert_root); + + + mProfilename = (EditText) findViewById(R.id.profilename); + mProfilenameLabel = (TextView) findViewById(R.id.profilename_label); + + if (savedInstanceState != null && savedInstanceState.containsKey(VPNPROFILE)) { + mResult = (VpnProfile) savedInstanceState.getSerializable(VPNPROFILE); + mAliasName = savedInstanceState.getString("mAliasName"); + mEmbeddedPwFile = savedInstanceState.getString("pwfile"); + mSourceUri = savedInstanceState.getParcelable("mSourceUri"); + mProfilename.setText(mResult.mName); + + if (savedInstanceState.containsKey("logentries")) { + //noinspection ConstantConditions + for (String logItem : savedInstanceState.getStringArray("logentries")) + log(logItem); + } + if (savedInstanceState.containsKey("fileselects")) { + //noinspection ConstantConditions + for (int k : savedInstanceState.getIntArray("fileselects")) { + addFileSelectDialog(Utils.FileType.getFileTypeByValue(k)); + } + } + return; + } + + + final android.content.Intent intent = getIntent(); + + if (intent != null) { + doImportIntent(intent); + + // We parsed the intent, relay on saved instance for restoring + setIntent(null); + } + + + } + + private void doImportIntent(Intent intent) { + final Uri data = intent.getData(); + if (data != null) { + mSourceUri = data; + doImportUri(data); + } + } + + private void doImportUri(Uri data) { + //log(R.string.import_experimental); + log(R.string.importing_config, data.toString()); + String possibleName = null; + if ((data.getScheme() != null && data.getScheme().equals("file")) || + (data.getLastPathSegment() != null && + (data.getLastPathSegment().endsWith(".ovpn") || + data.getLastPathSegment().endsWith(".conf"))) + ) { + possibleName = data.getLastPathSegment(); + if (possibleName.lastIndexOf('/') != -1) + possibleName = possibleName.substring(possibleName.lastIndexOf('/') + 1); + + } + + mPathsegments = data.getPathSegments(); + + Cursor cursor = getContentResolver().query(data, null, null, null, null); + + try { + + if (cursor != null && cursor.moveToFirst()) { + int columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + + if (columnIndex != -1) { + String displayName = cursor.getString(columnIndex); + if (displayName != null) + possibleName = displayName; + } + columnIndex = cursor.getColumnIndex("mime_type"); + if (columnIndex != -1) { + log("Mime type: " + cursor.getString(columnIndex)); + } + } + } finally { + if (cursor != null) + cursor.close(); + } + if (possibleName != null) { + possibleName = possibleName.replace(".ovpn", ""); + possibleName = possibleName.replace(".conf", ""); + } + + startImportTask(data, possibleName); + + + } + + private void startImportTask(final Uri data, final String possibleName) { + mImportTask = new AsyncTask<Void, Void, Integer>() { + private ProgressBar mProgress; + + @Override + protected void onPreExecute() { + mProgress = new ProgressBar(ConfigConverter.this); + addViewToLog(mProgress); + } + + @Override + protected Integer doInBackground(Void... params) { + try { + InputStream is = getContentResolver().openInputStream(data); + + doImport(is); + is.close(); + if (mResult==null) + return -3; + } catch (IOException| SecurityException se) + + { + log(R.string.import_content_resolve_error + ":" + se.getLocalizedMessage()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + checkMarschmallowFileImportError(data); + return -2; + } + + return 0; + } + + @Override + protected void onPostExecute(Integer errorCode) { + mLogLayout.removeView(mProgress); + addMissingFileDialogs(); + updateFileSelectDialogs(); + + if (errorCode == 0) { + displayWarnings(); + mResult.mName = getUniqueProfileName(possibleName); + mProfilename.setVisibility(View.VISIBLE); + mProfilenameLabel.setVisibility(View.VISIBLE); + mProfilename.setText(mResult.getName()); + + log(R.string.import_done); + } + } + }.execute(); + } + + + @TargetApi(Build.VERSION_CODES.M) + private void checkMarschmallowFileImportError(Uri data) { + // Permission already granted, not the source of the error + if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) + return; + + // We got a file:/// URL and have no permission to read it. Technically an error of the calling app since + // it makes an assumption about other apps being able to read the url but well ... + if (data != null && "file".equals(data.getScheme())) + doRequestSDCardPermission(PERMISSION_REQUEST_READ_URL); + + } + + + @Override + protected void onStart() { + super.onStart(); + } + + private void log(final String logmessage) { + runOnUiThread(new Runnable() { + @Override + public void run() { + TextView tv = new TextView(ConfigConverter.this); + mLogEntries.add(logmessage); + tv.setText(logmessage); + + addViewToLog(tv); + } + }); + } + + private void addViewToLog(View view) { + mLogLayout.addView(view, mLogLayout.getChildCount() - 1); + } + + private void doImport(InputStream is) { + ConfigParser cp = new ConfigParser(); + try { + InputStreamReader isr = new InputStreamReader(is); + + cp.parseConfig(isr); + mResult = cp.convertProfile(); + embedFiles(cp); + return; + + } catch (IOException | ConfigParseError e) { + log(R.string.error_reading_config_file); + log(e.getLocalizedMessage()); + } + mResult = null; + + } + + private void displayWarnings() { + if (mResult.mUseCustomConfig) { + log(R.string.import_warning_custom_options); + String copt = mResult.mCustomConfigOptions; + if (copt.startsWith("#")) { + int until = copt.indexOf('\n'); + copt = copt.substring(until + 1); + } + + log(copt); + } + + if (mResult.mAuthenticationType == VpnProfile.TYPE_KEYSTORE || + mResult.mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) { + findViewById(R.id.importpkcs12).setVisibility(View.VISIBLE); + } + + } + + private void log(int ressourceId, Object... formatArgs) { + log(getString(ressourceId, formatArgs)); + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java b/main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java new file mode 100644 index 00000000..e1cb8862 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.activities; + +import android.app.ListActivity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; +import de.blinkt.openvpn.LaunchVPN; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ProfileManager; + +import java.util.Collection; +import java.util.Vector; + +/** + * This Activity actually handles two stages of a launcher shortcut's life cycle. + * + * 1. Your application offers to provide shortcuts to the launcher. When + * the user installs a shortcut, an activity within your application + * generates the actual shortcut and returns it to the launcher, where it + * is shown to the user as an icon. + * + * 2. Any time the user clicks on an installed shortcut, an intent is sent. + * Typically this would then be handled as necessary by an activity within + * your application. + * + * We handle stage 1 (creating a shortcut) by simply sending back the information (in the form + * of an {@link android.content.Intent} that the launcher will use to create the shortcut. + * + * You can also implement this in an interactive way, by having your activity actually present + * UI for the user to select the specific nature of the shortcut, such as a contact, picture, URL, + * media item, or action. + * + * We handle stage 2 (responding to a shortcut) in this sample by simply displaying the contents + * of the incoming {@link android.content.Intent}. + * + * In a real application, you would probably use the shortcut intent to display specific content + * or start a particular operation. + */ +public class CreateShortcuts extends ListActivity implements OnItemClickListener { + + + private static final int START_VPN_PROFILE= 70; + + + private ProfileManager mPM; + private VpnProfile mSelectedProfile; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mPM =ProfileManager.getInstance(this); + + } + + @Override + protected void onStart() { + super.onStart(); + // Resolve the intent + + createListView(); + } + + private void createListView() { + ListView lv = getListView(); + //lv.setTextFilterEnabled(true); + + Collection<VpnProfile> vpnList = mPM.getProfiles(); + + Vector<String> vpnNames=new Vector<String>(); + for (VpnProfile vpnProfile : vpnList) { + vpnNames.add(vpnProfile.mName); + } + + + + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,vpnNames); + lv.setAdapter(adapter); + + lv.setOnItemClickListener(this); + } + + /** + * This function creates a shortcut and returns it to the caller. There are actually two + * intents that you will send back. + * + * The first intent serves as a container for the shortcut and is returned to the launcher by + * setResult(). This intent must contain three fields: + * + * <ul> + * <li>{@link android.content.Intent#EXTRA_SHORTCUT_INTENT} The shortcut intent.</li> + * <li>{@link android.content.Intent#EXTRA_SHORTCUT_NAME} The text that will be displayed with + * the shortcut.</li> + * <li>{@link android.content.Intent#EXTRA_SHORTCUT_ICON} The shortcut's icon, if provided as a + * bitmap, <i>or</i> {@link android.content.Intent#EXTRA_SHORTCUT_ICON_RESOURCE} if provided as + * a drawable resource.</li> + * </ul> + * + * If you use a simple drawable resource, note that you must wrapper it using + * {@link android.content.Intent.ShortcutIconResource}, as shown below. This is required so + * that the launcher can access resources that are stored in your application's .apk file. If + * you return a bitmap, such as a thumbnail, you can simply put the bitmap into the extras + * bundle using {@link android.content.Intent#EXTRA_SHORTCUT_ICON}. + * + * The shortcut intent can be any intent that you wish the launcher to send, when the user + * clicks on the shortcut. Typically this will be {@link android.content.Intent#ACTION_VIEW} + * with an appropriate Uri for your content, but any Intent will work here as long as it + * triggers the desired action within your Activity. + * @param profile + */ + private void setupShortcut(VpnProfile profile) { + // First, set up the shortcut intent. For this example, we simply create an intent that + // will bring us directly back to this activity. A more typical implementation would use a + // data Uri in order to display a more specific result, or a custom action in order to + // launch a specific operation. + + Intent shortcutIntent = new Intent(Intent.ACTION_MAIN); + shortcutIntent.setClass(this, LaunchVPN.class); + shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY,profile.getUUID().toString()); + + // Then, set up the container intent (the response to the caller) + + Intent intent = new Intent(); + intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, profile.getName()); + Parcelable iconResource = Intent.ShortcutIconResource.fromContext( + this, R.mipmap.ic_launcher); + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); + + // Now, return the result to the launcher + + setResult(RESULT_OK, intent); + } + + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + String profileName = ((TextView) view).getText().toString(); + + VpnProfile profile = mPM.getProfileByName(profileName); + + setupShortcut(profile); + finish(); + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java b/main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java new file mode 100644 index 00000000..80a134a9 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.activities; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.ActionBar.Tab; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.util.Base64; +import android.widget.Toast; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.fragments.FileSelectionFragment; +import de.blinkt.openvpn.fragments.InlineFileTab; + +public class FileSelect extends BaseActivity { + public static final String RESULT_DATA = "RESULT_PATH"; + public static final String START_DATA = "START_DATA"; + public static final String WINDOW_TITLE = "WINDOW_TILE"; + public static final String NO_INLINE_SELECTION = "de.blinkt.openvpn.NO_INLINE_SELECTION"; + public static final String SHOW_CLEAR_BUTTON = "de.blinkt.openvpn.SHOW_CLEAR_BUTTON"; + public static final String DO_BASE64_ENCODE = "de.blinkt.openvpn.BASE64ENCODE"; + private static final int PERMISSION_REQUEST = 23621; + + private FileSelectionFragment mFSFragment; + private InlineFileTab mInlineFragment; + private String mData; + private Tab inlineFileTab; + private Tab fileExplorerTab; + private boolean mNoInline; + private boolean mShowClear; + private boolean mBase64Encode; + + + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.file_dialog); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + checkPermission(); + + mData = getIntent().getStringExtra(START_DATA); + if(mData==null) + mData=Environment.getExternalStorageDirectory().getPath(); + + String title = getIntent().getStringExtra(WINDOW_TITLE); + int titleId = getIntent().getIntExtra(WINDOW_TITLE, 0); + if(titleId!=0) + title =getString(titleId); + if(title!=null) + setTitle(title); + + mNoInline = getIntent().getBooleanExtra(NO_INLINE_SELECTION, false); + mShowClear = getIntent().getBooleanExtra(SHOW_CLEAR_BUTTON, false); + mBase64Encode = getIntent().getBooleanExtra(DO_BASE64_ENCODE, false); + + ActionBar bar = getActionBar(); + bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + fileExplorerTab = bar.newTab().setText(R.string.file_explorer_tab); + inlineFileTab = bar.newTab().setText(R.string.inline_file_tab); + + mFSFragment = new FileSelectionFragment(); + fileExplorerTab.setTabListener(new MyTabsListener<FileSelectionFragment>(this, mFSFragment)); + bar.addTab(fileExplorerTab); + + if(!mNoInline) { + mInlineFragment = new InlineFileTab(); + inlineFileTab.setTabListener(new MyTabsListener<InlineFileTab>(this, mInlineFragment)); + bar.addTab(inlineFileTab); + } else { + mFSFragment.setNoInLine(); + } + + + } + + + @TargetApi(Build.VERSION_CODES.M) + private void checkPermission() { + if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + if (mNoInline) { + setResult(RESULT_CANCELED); + finish(); + } else { + if (fileExplorerTab!=null) + getActionBar().removeTab(fileExplorerTab); + } + } else { + mFSFragment.refresh(); + } + } + + public boolean showClear() { + if(mData == null || mData.equals("")) + return false; + else + return mShowClear; + } + + protected class MyTabsListener<T extends Fragment> implements ActionBar.TabListener + { + private Fragment mFragment; + private boolean mAdded=false; + + public MyTabsListener( Activity activity, Fragment fragment){ + this.mFragment = fragment; + } + + public void onTabSelected(Tab tab, FragmentTransaction ft) { + // Check if the fragment is already initialized + if (!mAdded) { + // If not, instantiate and add it to the activity + ft.add(android.R.id.content, mFragment); + mAdded =true; + } else { + // If it exists, simply attach it in order to show it + ft.attach(mFragment); + } + } + + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + ft.detach(mFragment); + } + + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) { + + } + } + + public void importFile(String path) { + File ifile = new File(path); + Exception fe = null; + try { + + String data = ""; + + byte[] fileData = readBytesFromFile(ifile) ; + if(mBase64Encode) + data += Base64.encodeToString(fileData, Base64.DEFAULT); + else + data += new String(fileData); + + mData =data; + + /* + mInlineFragment.setData(data); + getActionBar().selectTab(inlineFileTab); */ + saveInlineData(ifile.getName(), data); + } catch (IOException e) { + fe =e; + } + if(fe!=null) { + Builder ab = new AlertDialog.Builder(this); + ab.setTitle(R.string.error_importing_file); + ab.setMessage(getString(R.string.import_error_message) + "\n" + fe.getLocalizedMessage()); + ab.setPositiveButton(android.R.string.ok, null); + ab.show(); + } + } + + static private byte[] readBytesFromFile(File file) throws IOException { + InputStream input = new FileInputStream(file); + + long len= file.length(); + if (len > VpnProfile.MAX_EMBED_FILE_SIZE) + throw new IOException("selected file size too big to embed into profile"); + + // Create the byte array to hold the data + byte[] bytes = new byte[(int) len]; + + // Read in the bytes + int offset = 0; + int bytesRead = 0; + while (offset < bytes.length + && (bytesRead=input.read(bytes, offset, bytes.length-offset)) >= 0) { + offset += bytesRead; + } + + input.close(); + return bytes; + } + + + public void setFile(String path) { + Intent intent = new Intent(); + intent.putExtra(RESULT_DATA, path); + setResult(Activity.RESULT_OK,intent); + finish(); + } + + public String getSelectPath() { + if(VpnProfile.isEmbedded(mData)) + return mData; + else + return Environment.getExternalStorageDirectory().getPath(); + } + + public CharSequence getInlineData() { + if(VpnProfile.isEmbedded(mData)) + return VpnProfile.getEmbeddedContent(mData); + else + return ""; + } + + public void clearData() { + Intent intent = new Intent(); + intent.putExtra(RESULT_DATA, (String)null); + setResult(Activity.RESULT_OK,intent); + finish(); + + } + + public void saveInlineData(String fileName, String string) { + Intent intent = new Intent(); + + if(fileName==null) + intent.putExtra(RESULT_DATA, VpnProfile.INLINE_TAG + string); + else + intent.putExtra(RESULT_DATA,VpnProfile.DISPLAYNAME_TAG + fileName + VpnProfile.INLINE_TAG + string); + setResult(Activity.RESULT_OK, intent); + finish(); + + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java b/main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java new file mode 100644 index 00000000..db70eca9 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.activities; + +import android.app.Activity; +import android.os.Bundle; +import android.view.MenuItem; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.fragments.LogFragment; + +/** + * Created by arne on 13.10.13. + */ +public class LogWindow extends BaseActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.log_window); + getActionBar().setDisplayHomeAsUpEnabled(true); + + if (savedInstanceState == null) { + getFragmentManager().beginTransaction() + .add(R.id.container, new LogFragment()) + .commit(); + } + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return super.onOptionsItemSelected(item); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java b/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java new file mode 100644 index 00000000..f7c46d01 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.activities; + +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.PowerManager; +import android.provider.Settings; +import android.support.v4n.view.ViewPager; +import android.view.Menu; +import android.view.MenuItem; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.fragments.AboutFragment; +import de.blinkt.openvpn.fragments.FaqFragment; +import de.blinkt.openvpn.fragments.GeneralSettings; +import de.blinkt.openvpn.fragments.GraphFragment; +import de.blinkt.openvpn.fragments.LogFragment; +import de.blinkt.openvpn.fragments.SendDumpFragment; +import de.blinkt.openvpn.fragments.VPNProfileList; +import de.blinkt.openvpn.views.ScreenSlidePagerAdapter; +import de.blinkt.openvpn.views.SlidingTabLayout; +import de.blinkt.openvpn.views.TabBarView; + + +public class MainActivity extends BaseActivity { + + private ViewPager mPager; + private ScreenSlidePagerAdapter mPagerAdapter; + private SlidingTabLayout mSlidingTabLayout; + private TabBarView mTabs; + + protected void onCreate(android.os.Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.main_activity); + + + // Instantiate a ViewPager and a PagerAdapter. + mPager = (ViewPager) findViewById(R.id.pager); + mPagerAdapter = new ScreenSlidePagerAdapter(getFragmentManager(), this); + + /* Toolbar and slider should have the same elevation */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + disableToolbarElevation(); + } + + + mPagerAdapter.addTab(R.string.vpn_list_title, VPNProfileList.class); + mPagerAdapter.addTab(R.string.graph, GraphFragment.class); + + mPagerAdapter.addTab(R.string.generalsettings, GeneralSettings.class); + mPagerAdapter.addTab(R.string.faq, FaqFragment.class); + + if (SendDumpFragment.getLastestDump(this) != null) { + mPagerAdapter.addTab(R.string.crashdump, SendDumpFragment.class); + } + + + if (isDirectToTV()) + mPagerAdapter.addTab(R.string.openvpn_log, LogFragment.class); + + mPagerAdapter.addTab(R.string.about, AboutFragment.class); + mPager.setAdapter(mPagerAdapter); + + mTabs = (TabBarView) findViewById(R.id.sliding_tabs); + mTabs.setViewPager(mPager); + } + + private static final String FEATURE_TELEVISION = "android.hardware.type.television"; + private static final String FEATURE_LEANBACK = "android.software.leanback"; + + private boolean isDirectToTV() { + return(getPackageManager().hasSystemFeature(FEATURE_TELEVISION) + || getPackageManager().hasSystemFeature(FEATURE_LEANBACK)); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void disableToolbarElevation() { + ActionBar toolbar = getActionBar(); + toolbar.setElevation(0); + } + + @Override + protected void onResume() { + super.onResume(); + if (getIntent()!=null) { + String page = getIntent().getStringExtra("PAGE"); + if ("graph".equals(page)) { + mPager.setCurrentItem(1); + } + setIntent(null); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu,menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId()==R.id.show_log){ + Intent showLog = new Intent(this, LogWindow.class); + startActivity(showLog); + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + System.out.println(data); + + + } + + + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java b/main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java new file mode 100644 index 00000000..4720dd60 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2012-2017 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.activities; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.Locale; +import java.util.Vector; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.core.NativeUtils; +import de.blinkt.openvpn.core.OpenVPNService; + +public class OpenSSLSpeed extends Activity { + + private static SpeeedTest runTestAlgorithms; + private EditText mCipher; + private SpeedArrayAdapter mAdapter; + private ListView mListView; + + + static class SpeedArrayAdapter extends ArrayAdapter<SpeedResult> { + + private final Context mContext; + private final LayoutInflater mInflater; + + public SpeedArrayAdapter(@NonNull Context context) { + super(context, 0); + mContext = context; + mInflater = LayoutInflater.from(context); + + } + + class ViewHolder { + TextView ciphername; + TextView blocksize; + TextView blocksInTime; + TextView speed; + } + + @NonNull + @Override + public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) { + SpeedResult res = getItem(position); + if (view == null) { + view = mInflater.inflate(R.layout.speedviewitem, parent, false); + ViewHolder holder = new ViewHolder(); + holder.ciphername = view.findViewById(R.id.ciphername); + holder.speed = view.findViewById(R.id.speed); + holder.blocksize = view.findViewById(R.id.blocksize); + holder.blocksInTime = view.findViewById(R.id.blocksintime); + view.setTag(holder); + } + + ViewHolder holder = (ViewHolder) view.getTag(); + + double total = res.count * res.length; + String size = OpenVPNService.humanReadableByteCount((long) res.length, false, mContext.getResources()); + + holder.blocksize.setText(size); + holder.ciphername.setText(res.algorithm); + + if (res.failed) { + holder.blocksInTime.setText(R.string.openssl_error); + holder.speed.setText("-"); + } else if (res.running) { + holder.blocksInTime.setText(R.string.running_test); + holder.speed.setText("-"); + } else { + String totalBytes = OpenVPNService.humanReadableByteCount((long) total, false, mContext.getResources()); + // TODO: Fix localisation here + String blockPerSec = OpenVPNService.humanReadableByteCount((long) (total / res.time), false, mContext.getResources()) + "/s"; + holder.speed.setText(blockPerSec); + holder.blocksInTime.setText(String.format(Locale.ENGLISH, "%d blocks (%s) in %2.1fs", (long) res.count, totalBytes, res.time)); + } + + return view; + + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.openssl_speed); + getActionBar().setDisplayHomeAsUpEnabled(true); + + findViewById(R.id.testSpecific).setOnClickListener((view) -> { + runAlgorithms(mCipher.getText().toString()); + }); + mCipher = (EditText) findViewById(R.id.ciphername); + + mListView = findViewById(R.id.results); + + mAdapter = new SpeedArrayAdapter(this); + mListView.setAdapter(mAdapter); + + } + + private void runAlgorithms(String algorithms) { + if (runTestAlgorithms != null) + runTestAlgorithms.cancel(true); + runTestAlgorithms = new SpeeedTest(); + runTestAlgorithms.execute(algorithms.split(" ")); + } + + + static class SpeedResult { + String algorithm; + boolean failed = false; + + double count; + double time; + int length; + public boolean running=true; + + SpeedResult(String algorithm) { + this.algorithm = algorithm; + } + } + + + private class SpeeedTest extends AsyncTask<String, SpeedResult, SpeedResult[]> { + + + private boolean mCancel = false; + + @Override + protected SpeedResult[] doInBackground(String... strings) { + Vector<SpeedResult> mResult = new Vector<>(); + + for (String algorithm : strings) { + + // Skip 16b and 16k as they are not relevevant for VPN + for (int i = 1; i < NativeUtils.openSSLlengths.length -1 && !mCancel; i++) { + SpeedResult result = new SpeedResult(algorithm); + result.length = NativeUtils.openSSLlengths[i]; + mResult.add(result); + publishProgress(result); + double[] resi = NativeUtils.getOpenSSLSpeed(algorithm, i); + if (resi == null) { + result.failed = true; + } else { + result.count = resi[1]; + result.time = resi[2]; + } + result.running = false; + publishProgress(result); + } + } + + return mResult.toArray(new SpeedResult[mResult.size()]); + + } + + @Override + protected void onProgressUpdate(SpeedResult... values) { + for (SpeedResult r : values) { + if (r.running) + mAdapter.add(r); + mAdapter.notifyDataSetChanged(); + } + } + + @Override + protected void onPostExecute(SpeedResult[] speedResult) { + + } + + @Override + protected void onCancelled(SpeedResult[] speedResults) { + mCancel = true; + } + } + + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java b/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java new file mode 100644 index 00000000..06f1f7b7 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.activities; + +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.support.v4n.view.ViewPager; +import android.view.Menu; +import android.view.MenuItem; + +import android.widget.Toast; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ProfileManager; +import de.blinkt.openvpn.fragments.Settings_Allowed_Apps; +import de.blinkt.openvpn.fragments.Settings_Authentication; +import de.blinkt.openvpn.fragments.Settings_Basic; +import de.blinkt.openvpn.fragments.Settings_Connections; +import de.blinkt.openvpn.fragments.Settings_IP; +import de.blinkt.openvpn.fragments.Settings_Obscure; +import de.blinkt.openvpn.fragments.Settings_Routing; +import de.blinkt.openvpn.fragments.Settings_UserEditable; +import de.blinkt.openvpn.fragments.ShowConfigFragment; +import de.blinkt.openvpn.fragments.VPNProfileList; +import de.blinkt.openvpn.views.ScreenSlidePagerAdapter; +import de.blinkt.openvpn.views.TabBarView; + + +public class VPNPreferences extends BaseActivity { + + static final Class validFragments[] = new Class[] { + Settings_Authentication.class, Settings_Basic.class, Settings_IP.class, + Settings_Obscure.class, Settings_Routing.class, ShowConfigFragment.class, + Settings_Connections.class, Settings_Allowed_Apps.class + }; + + private String mProfileUUID; + private VpnProfile mProfile; + private ViewPager mPager; + private ScreenSlidePagerAdapter mPagerAdapter; + + public VPNPreferences() { + super(); + } + + + @TargetApi(Build.VERSION_CODES.KITKAT) + protected boolean isValidFragment(String fragmentName) { + for (Class c: validFragments) + if (c.getName().equals(fragmentName)) + return true; + return false; + + } + + @Override + protected void onStop() { + super.onStop(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putString(getIntent().getStringExtra(getPackageName() + ".profileUUID"),mProfileUUID); + super.onSaveInstanceState(outState); + } + + @Override + protected void onResume() { + super.onResume(); + getProfile(); + // When a profile is deleted from a category fragment in hadset mod we need to finish + // this activity as well when returning + if (mProfile==null || mProfile.profileDeleted) { + setResult(VPNProfileList.RESULT_VPN_DELETED); + finish(); + } + if (mProfile.mTemporaryProfile) + { + Toast.makeText(this, "Temporary profiles cannot be edited", Toast.LENGTH_LONG); + finish(); + } + } + + private void getProfile() { + Intent intent = getIntent(); + + if(intent!=null) { + String profileUUID = intent.getStringExtra(getPackageName() + ".profileUUID"); + if(profileUUID==null) { + Bundle initialArguments = getIntent().getBundleExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + profileUUID = initialArguments.getString(getPackageName() + ".profileUUID"); + } + if(profileUUID!=null){ + + mProfileUUID = profileUUID; + mProfile = ProfileManager.get(this, mProfileUUID); + + } + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + mProfileUUID = getIntent().getStringExtra(getPackageName() + ".profileUUID"); + if(savedInstanceState!=null){ + String savedUUID = savedInstanceState.getString(getPackageName() + ".profileUUID"); + if(savedUUID!=null) + mProfileUUID=savedUUID; + } + + mProfile = ProfileManager.get(this,mProfileUUID); + if(mProfile!=null) { + setTitle(getString(R.string.edit_profile_title, mProfile.getName())); + } + super.onCreate(savedInstanceState); + + + setContentView(R.layout.main_activity); + + /* Toolbar and slider should have the same elevation */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + disableToolbarElevation(); + } + + // Instantiate a ViewPager and a PagerAdapter. + mPager = (ViewPager) findViewById(R.id.pager); + mPagerAdapter = new ScreenSlidePagerAdapter(getFragmentManager(), this); + + + Bundle fragmentArguments = new Bundle(); + fragmentArguments.putString(getPackageName() + ".profileUUID",mProfileUUID); + mPagerAdapter.setFragmentArgs(fragmentArguments); + + if (mProfile.mUserEditable) { + mPagerAdapter.addTab(R.string.basic, Settings_Basic.class); + mPagerAdapter.addTab(R.string.server_list, Settings_Connections.class); + mPagerAdapter.addTab(R.string.ipdns, Settings_IP.class); + mPagerAdapter.addTab(R.string.routing, Settings_Routing.class); + mPagerAdapter.addTab(R.string.settings_auth, Settings_Authentication.class); + + mPagerAdapter.addTab(R.string.advanced, Settings_Obscure.class); + } else { + mPagerAdapter.addTab(R.string.basic, Settings_UserEditable.class); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + mPagerAdapter.addTab(R.string.vpn_allowed_apps, Settings_Allowed_Apps.class); + + mPagerAdapter.addTab(R.string.generated_config, ShowConfigFragment.class); + + + mPager.setAdapter(mPagerAdapter); + + TabBarView tabs = (TabBarView) findViewById(R.id.sliding_tabs); + tabs.setViewPager(mPager); + + } + + +/* + @Override + public void onBuildHeaders(List<Header> target) { + loadHeadersFromResource(R.xml.vpn_headers, target); + Header headerToRemove=null; + for (Header header : target) { + if(header.fragmentArguments==null) + header.fragmentArguments = new Bundle(); + header.fragmentArguments.putString(getPackageName() + ".profileUUID",mProfileUUID); + if (header.id == R.id.allowed_apps_header && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + headerToRemove = header; + } + if (headerToRemove != null) + target.remove(headerToRemove); + }*/ + + @Override + public void onBackPressed() { + setResult(RESULT_OK, getIntent()); + super.onBackPressed(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.remove_vpn) + askProfileRemoval(); + if (item.getItemId() == R.id.duplicate_vpn) { + Intent data = new Intent(); + data.putExtra(VpnProfile.EXTRA_PROFILEUUID, mProfileUUID); + setResult(VPNProfileList.RESULT_VPN_DUPLICATE, data); + finish(); + } + + return super.onOptionsItemSelected(item); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + getMenuInflater().inflate(R.menu.vpnpreferences_menu, menu); + + return super.onCreateOptionsMenu(menu); + } + + private void askProfileRemoval() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle("Confirm deletion"); + dialog.setMessage(getString(R.string.remove_vpn_query, mProfile.mName)); + + dialog.setPositiveButton(android.R.string.yes, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + removeProfile(mProfile); + } + + }); + dialog.setNegativeButton(android.R.string.no,null); + dialog.create().show(); + } + + protected void removeProfile(VpnProfile profile) { + ProfileManager.getInstance(this).removeProfile(this,profile); + setResult(VPNProfileList.RESULT_VPN_DELETED); + finish(); + + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void disableToolbarElevation() { + ActionBar toolbar = getActionBar(); + toolbar.setElevation(0); + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java new file mode 100644 index 00000000..540f4a9a --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Fragment; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentSender; +import android.content.ServiceConnection; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.vending.billing.IInAppBillingService; + +import de.blinkt.openvpn.core.NativeUtils; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Locale; +import java.util.Vector; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.core.VpnStatus; + +public class AboutFragment extends Fragment implements View.OnClickListener { + + public static final String INAPPITEM_TYPE_INAPP = "inapp"; + public static final String RESPONSE_CODE = "RESPONSE_CODE"; + private static final int DONATION_CODE = 12; + private static final int BILLING_RESPONSE_RESULT_OK = 0; + private static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; + private static final String[] donationSkus = { "donation1eur", "donation2eur", "donation5eur", "donation10eur", + "donation1337eur","donation23eur","donation25eur",}; + IInAppBillingService mService; + Hashtable<View, String> viewToProduct = new Hashtable<>(); + ServiceConnection mServiceConn = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + + @Override + public void onServiceConnected(ComponentName name, + IBinder service) { + mService = IInAppBillingService.Stub.asInterface(service); + initGooglePlayDonation(); + + } + }; + + private void initGooglePlayDonation() { + new Thread("queryGMSInApp") { + @Override + public void run() { + initGMSDonateOptions(); + } + }.start(); + } + + private TextView gmsTextView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + /* + getActivity().bindService(new + Intent("com.android.vending.billing.InAppBillingService.BIND"), + mServiceConn, Context.BIND_AUTO_CREATE); + */ + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mService != null) { + getActivity().unbindService(mServiceConn); + } + + } + + private void initGMSDonateOptions() { + try { + int billingSupported = mService.isBillingSupported(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP); + if (billingSupported != BILLING_RESPONSE_RESULT_OK) { + Log.i("OpenVPN", "Play store billing not supported"); + return; + } + + ArrayList skuList = new ArrayList(); + Collections.addAll(skuList, donationSkus); + Bundle querySkus = new Bundle(); + querySkus.putStringArrayList("ITEM_ID_LIST", skuList); + + Bundle ownedItems = mService.getPurchases(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP, null); + + + if (ownedItems.getInt(RESPONSE_CODE) != BILLING_RESPONSE_RESULT_OK) + return; + + final ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST"); + + Bundle skuDetails = mService.getSkuDetails(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP, querySkus); + + + if (skuDetails.getInt(RESPONSE_CODE) != BILLING_RESPONSE_RESULT_OK) + return; + + final ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST"); + + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + createPlayBuyOptions(ownedSkus, responseList); + + } + }); + } + + } catch (RemoteException e) { + VpnStatus.logException(e); + } + } + + private static class SkuResponse { + String title; + String price; + + SkuResponse(String p, String t) + { + title=t; + price=p; + } + } + + + + private void createPlayBuyOptions(ArrayList<String> ownedSkus, ArrayList<String> responseList) { + try { + Vector<Pair<String,String>> gdonation = new Vector<Pair<String, String>>(); + + gdonation.add(new Pair<String, String>(getString(R.string.donatePlayStore),null)); + HashMap<String, SkuResponse> responseMap = new HashMap<String, SkuResponse>(); + for (String thisResponse : responseList) { + JSONObject object = new JSONObject(thisResponse); + responseMap.put( + object.getString("productId"), + new SkuResponse( + object.getString("price"), + object.getString("title"))); + + } + for (String sku: donationSkus) + if (responseMap.containsKey(sku)) + gdonation.add(getSkuTitle(sku, + responseMap.get(sku).title, responseMap.get(sku).price, ownedSkus)); + + String gmsTextString=""; + for(int i=0;i<gdonation.size();i++) { + if(i==1) + gmsTextString+= " "; + else if(i>1) + gmsTextString+= ", "; + gmsTextString+=gdonation.elementAt(i).first; + } + SpannableString gmsText = new SpannableString(gmsTextString); + + + int lStart = 0; + int lEnd=0; + for(Pair<String, String> item:gdonation){ + lEnd = lStart + item.first.length(); + if (item.second!=null) { + final String mSku = item.second; + ClickableSpan cspan = new ClickableSpan() + { + @Override + public void onClick(View widget) { + triggerBuy(mSku); + } + }; + gmsText.setSpan(cspan,lStart,lEnd,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + lStart = lEnd+2; // Account for ", " between items + } + + if(gmsTextView !=null) { + gmsTextView.setText(gmsText); + gmsTextView.setMovementMethod(LinkMovementMethod.getInstance()); + gmsTextView.setVisibility(View.VISIBLE); + } + + } catch (JSONException e) { + VpnStatus.logException("Parsing Play Store IAP",e); + } + + } + + private Pair<String,String> getSkuTitle(final String sku, String title, String price, ArrayList<String> ownedSkus) { + String text; + if (ownedSkus.contains(sku)) + return new Pair<String,String>(getString(R.string.thanks_for_donation, price),null); + + if (price.contains("€")|| price.contains("\u20ac")) { + text= title; + } else { + text = String.format(Locale.getDefault(), "%s (%s)", title, price); + } + //return text; + return new Pair<String,String>(price, sku); + + } + + private void triggerBuy(String sku) { + try { + Bundle buyBundle + = mService.getBuyIntent(3, getActivity().getPackageName(), + sku, INAPPITEM_TYPE_INAPP, "Thanks for the donation! :)"); + + + if (buyBundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) { + PendingIntent buyIntent = buyBundle.getParcelable(RESPONSE_BUY_INTENT); + getActivity().startIntentSenderForResult(buyIntent.getIntentSender(), DONATION_CODE, new Intent(), + 0, 0, 0); + } + + } catch (RemoteException e) { + VpnStatus.logException(e); + } catch (IntentSender.SendIntentException e) { + VpnStatus.logException(e); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.about, container, false); + TextView ver = (TextView) v.findViewById(R.id.version); + + String version; + String name = "Openvpn"; + try { + PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0); + version = packageinfo.versionName; + name = getString(R.string.app); + } catch (NameNotFoundException e) { + version = "error fetching version"; + } + + ver.setText(getString(R.string.version_info, name, version)); + + TextView verO2 = v.findViewById(R.id.version_ovpn2); + TextView verO3 = v.findViewById(R.id.version_ovpn3); + + verO2.setText(String.format(Locale.US, "OpenVPN version: %s", NativeUtils.getOpenVPN2GitVersion())); + verO3.setText(String.format(Locale.US, "OpenVPN3 core version: %s", NativeUtils.getOpenVPN3GitVersion())); + + gmsTextView = (TextView) v.findViewById(R.id.donategms); + /* recreating view without onCreate/onDestroy cycle */ + + // Disable GMS for now + if (mService!=null) + initGooglePlayDonation(); + + TextView translation = (TextView) v.findViewById(R.id.translation); + + // Don't print a text for myself + if (getString(R.string.translationby).contains("Arne Schwabe")) + translation.setText(""); + else + translation.setText(R.string.translationby); + + TextView wv = (TextView) v.findViewById(R.id.full_licenses); + wv.setText(Html.fromHtml(readHtmlFromAssets())); + return v; + } + + String readHtmlFromAssets() + { + InputStream mvpn; + + try { + mvpn = getActivity().getAssets().open("full_licenses.html"); + BufferedReader reader = new BufferedReader(new InputStreamReader(mvpn)); + StringBuilder sb = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } catch (IOException errabi) { + return "full_licenses.html not found"; + } + } + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (mService!=null) + initGooglePlayDonation(); + } + + + @Override + public void onClick(View v) { + + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java b/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java new file mode 100644 index 00000000..9c4c80de --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.AlertDialog; +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.*; + +import java.util.Arrays; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.Connection; + +public class ConnectionsAdapter extends RecyclerView.Adapter<ConnectionsAdapter.ConnectionsHolder> { + private static final int TYPE_NORMAL = 0; + private static final int TYPE_FOOTER = TYPE_NORMAL + 1; + private final Context mContext; + private final VpnProfile mProfile; + private final Settings_Connections mConnectionFragment; + private Connection[] mConnections; + + ConnectionsAdapter(Context c, Settings_Connections connections_fragments, VpnProfile vpnProfile) { + mContext = c; + mConnections = vpnProfile.mConnections; + mProfile = vpnProfile; + mConnectionFragment = connections_fragments; + } + + @Override + public ConnectionsAdapter.ConnectionsHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + LayoutInflater li = LayoutInflater.from(mContext); + + View card; + if (viewType == TYPE_NORMAL) { + card = li.inflate(R.layout.server_card, viewGroup, false); + + } else { // TYPE_FOOTER + card = li.inflate(R.layout.server_footer, viewGroup, false); + } + return new ConnectionsHolder(card, this, viewType); + + } + + @Override + public void onBindViewHolder(final ConnectionsAdapter.ConnectionsHolder cH, int position) { + if (position == mConnections.length) { + // Footer + return; + } + final Connection connection = mConnections[position]; + + cH.mConnection = null; + + cH.mPortNumberView.setText(connection.mServerPort); + cH.mServerNameView.setText(connection.mServerName); + cH.mPortNumberView.setText(connection.mServerPort); + cH.mRemoteSwitch.setChecked(connection.mEnabled); + + + cH.mProxyNameView.setText(connection.mProxyName); + cH.mProxyPortNumberView.setText(connection.mProxyPort); + + cH.mConnectText.setText(String.valueOf(connection.getTimeout())); + + cH.mConnectSlider.setProgress(connection.getTimeout()); + + + cH.mProtoGroup.check(connection.mUseUdp ? R.id.udp_proto : R.id.tcp_proto); + + switch (connection.mProxyType) { + case NONE: + cH.mProxyGroup.check(R.id.proxy_none); + break; + case HTTP: + cH.mProxyGroup.check(R.id.proxy_http); + break; + case SOCKS5: + cH.mProxyGroup.check(R.id.proxy_http); + break; + case ORBOT: + cH.mProxyGroup.check(R.id.proxy_orbot); + break; + } + + cH.mProxyAuthCb.setChecked(connection.mUseProxyAuth); + cH.mProxyAuthUser.setText(connection.mProxyAuthUser); + cH.mProxyAuthPassword.setText(connection.mProxyAuthPassword); + + cH.mCustomOptionsLayout.setVisibility(connection.mUseCustomConfig ? View.VISIBLE : View.GONE); + cH.mCustomOptionText.setText(connection.mCustomConfiguration); + + cH.mCustomOptionCB.setChecked(connection.mUseCustomConfig); + cH.mConnection = connection; + + setVisibilityProxyServer(cH, connection); + + } + + private void setVisibilityProxyServer(ConnectionsHolder cH, Connection connection) { + int visible = (connection.mProxyType == Connection.ProxyType.HTTP || connection.mProxyType == Connection.ProxyType.SOCKS5) ? View.VISIBLE : View.GONE; + int authVisible = (connection.mProxyType == Connection.ProxyType.HTTP) ? View.VISIBLE : View.GONE; + + cH.mProxyNameView.setVisibility(visible); + cH.mProxyPortNumberView.setVisibility(visible); + cH.mProxyNameLabel.setVisibility(visible); + + cH.mProxyAuthLayout.setVisibility(authVisible); + + } + + private void removeRemote(int idx) { + Connection[] mConnections2 = Arrays.copyOf(mConnections, mConnections.length - 1); + for (int i = idx + 1; i < mConnections.length; i++) { + mConnections2[i - 1] = mConnections[i]; + } + mConnections = mConnections2; + + } + + @Override + public int getItemCount() { + return mConnections.length + 1; //for footer + } + + @Override + public int getItemViewType(int position) { + if (position == mConnections.length) + return TYPE_FOOTER; + else + return TYPE_NORMAL; + } + + void addRemote() { + mConnections = Arrays.copyOf(mConnections, mConnections.length + 1); + mConnections[mConnections.length - 1] = new Connection(); + notifyItemInserted(mConnections.length - 1); + displayWarningIfNoneEnabled(); + } + + void displayWarningIfNoneEnabled() { + int showWarning = View.VISIBLE; + for (Connection conn : mConnections) { + if (conn.mEnabled) + showWarning = View.GONE; + } + mConnectionFragment.setWarningVisible(showWarning); + } + + void saveProfile() { + mProfile.mConnections = mConnections; + } + + static abstract class OnTextChangedWatcher implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + } + + class ConnectionsHolder extends RecyclerView.ViewHolder { + private final EditText mServerNameView; + private final EditText mPortNumberView; + private final Switch mRemoteSwitch; + private final RadioGroup mProtoGroup; + private final EditText mCustomOptionText; + private final CheckBox mCustomOptionCB; + private final View mCustomOptionsLayout; + private final ImageButton mDeleteButton; + private final EditText mConnectText; + private final SeekBar mConnectSlider; + private final ConnectionsAdapter mConnectionsAdapter; + private final RadioGroup mProxyGroup; + private final EditText mProxyNameView; + private final EditText mProxyPortNumberView; + private final View mProxyNameLabel; + private final View mProxyAuthLayout; + private final EditText mProxyAuthUser; + private final EditText mProxyAuthPassword; + private final CheckBox mProxyAuthCb; + + private Connection mConnection; // Set to null on update + + + ConnectionsHolder(View card, ConnectionsAdapter connectionsAdapter, int viewType) { + super(card); + mServerNameView = card.findViewById(R.id.servername); + mPortNumberView = card.findViewById(R.id.portnumber); + mRemoteSwitch = card.findViewById(R.id.remoteSwitch); + mCustomOptionCB = card.findViewById(R.id.use_customoptions); + mCustomOptionText = card.findViewById(R.id.customoptions); + mProtoGroup = card.findViewById(R.id.udptcpradiogroup); + mCustomOptionsLayout = card.findViewById(R.id.custom_options_layout); + mDeleteButton = card.findViewById(R.id.remove_connection); + mConnectSlider = card.findViewById(R.id.connect_silder); + mConnectText = card.findViewById(R.id.connect_timeout); + + mProxyGroup = card.findViewById(R.id.proxyradiogroup); + mProxyNameView = card.findViewById(R.id.proxyname); + mProxyPortNumberView = card.findViewById(R.id.proxyport); + mProxyNameLabel = card.findViewById(R.id.proxyserver_label); + + mProxyAuthLayout = card.findViewById(R.id.proxyauthlayout); + mProxyAuthCb = card.findViewById(R.id.enable_proxy_auth); + mProxyAuthUser = card.findViewById(R.id.proxyuser); + mProxyAuthPassword = card.findViewById(R.id.proxypassword); + + mConnectionsAdapter = connectionsAdapter; + + if (viewType == TYPE_NORMAL) + addListeners(); + } + + + void addListeners() { + mRemoteSwitch.setOnCheckedChangeListener((CompoundButton buttonView, boolean isChecked) -> { + if (mConnection != null) { + mConnection.mEnabled = isChecked; + mConnectionsAdapter.displayWarningIfNoneEnabled(); + } + }); + + mProtoGroup.setOnCheckedChangeListener((group, checkedId) -> { + if (mConnection != null) { + if (checkedId == R.id.udp_proto) + mConnection.mUseUdp = true; + else if (checkedId == R.id.tcp_proto) + mConnection.mUseUdp = false; + } + }); + + mProxyGroup.setOnCheckedChangeListener((group, checkedId) -> { + if (mConnection != null) { + switch (checkedId) { + case R.id.proxy_none: + mConnection.mProxyType = Connection.ProxyType.NONE; + break; + case R.id.proxy_http: + mConnection.mProxyType = Connection.ProxyType.HTTP; + break; + case R.id.proxy_socks: + mConnection.mProxyType = Connection.ProxyType.SOCKS5; + break; + case R.id.proxy_orbot: + mConnection.mProxyType = Connection.ProxyType.ORBOT; + break; + } + setVisibilityProxyServer(this, mConnection); + } + }); + + mProxyAuthCb.setOnCheckedChangeListener((group, isChecked) -> + { + if (mConnection != null) { + mConnection.mUseProxyAuth = isChecked; + setVisibilityProxyServer(this, mConnection); + } + }); + + mCustomOptionText.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) + mConnection.mCustomConfiguration = s.toString(); + } + }); + + mCustomOptionCB.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (mConnection != null) { + mConnection.mUseCustomConfig = isChecked; + mCustomOptionsLayout.setVisibility(mConnection.mUseCustomConfig ? View.VISIBLE : View.GONE); + } + }); + + + mServerNameView.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) { + mConnection.mServerName = s.toString(); + } + } + + }); + + mPortNumberView.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) { + mConnection.mServerPort = s.toString(); + } + } + }); + + mProxyNameView.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) { + mConnection.mProxyName = s.toString(); + } + } + + }); + + mProxyPortNumberView.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) { + mConnection.mProxyPort = s.toString(); + } + } + }); + + mCustomOptionCB.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (mConnection != null) { + mConnection.mUseProxyAuth = isChecked; + } + }); + + mProxyAuthPassword.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) { + mConnection.mProxyAuthPassword = s.toString(); + } + } + }); + + + mProxyAuthUser.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) { + mConnection.mProxyAuthUser = s.toString(); + } + } + }); + + + + mCustomOptionText.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) { + mConnection.mCustomConfiguration = s.toString(); + } + } + }); + + mConnectSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser && mConnection != null) { + mConnectText.setText(String.valueOf(progress)); + mConnection.mConnectTimeout = progress; + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + mConnectText.addTextChangedListener(new OnTextChangedWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (mConnection != null) { + try { + int t = Integer.valueOf(String.valueOf(s)); + mConnectSlider.setProgress(t); + mConnection.mConnectTimeout = t; + } catch (Exception ignored) { + } + } + } + }); + + mDeleteButton.setOnClickListener( + v -> { + AlertDialog.Builder ab = new AlertDialog.Builder(mContext); + ab.setTitle(R.string.query_delete_remote); + ab.setPositiveButton(R.string.keep, null); + ab.setNegativeButton(R.string.delete, (dialog, which) -> { + removeRemote(getAdapterPosition()); + notifyItemRemoved(getAdapterPosition()); + }); + ab.create().show(); + } + ); + + + } + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java new file mode 100644 index 00000000..af4c35fe --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Fragment; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Vector; + +import de.blinkt.openvpn.R; + +public class FaqFragment extends Fragment { + static class FAQEntry { + + public FAQEntry(int startVersion, int endVersion, int title, int description) { + this.startVersion = startVersion; + this.endVersion = endVersion; + this.description = description; + this.title = title; + } + + final int startVersion; + final int endVersion; + final int description; + final int title; + + public boolean runningVersion() { + if (Build.VERSION.SDK_INT >= startVersion) { + if (Build.VERSION.SDK_INT <= endVersion) + return true; + + if (endVersion == -1) + return true; + + String release = Build.VERSION.RELEASE; + boolean isOlderThan443 = !release.startsWith("4.4.3") && !release.startsWith("4.4.4") && + !release.startsWith("4.4.5") && !release.startsWith("4.4.6"); + + boolean isOlderThan442 = isOlderThan443 && !release.startsWith("4.4.2"); + + + if(Build.VERSION.SDK_INT== Build.VERSION_CODES.KITKAT) { + if (endVersion == -441 && isOlderThan442) + return true; + + if (endVersion == -442 && isOlderThan443) + return true; + } else if (endVersion == -441 || endVersion == -442) { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT; + } + + + } + return false; + } + + public String getVersionsString(Context c) { + if (startVersion == Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (endVersion == -1) + return null; + else + return c.getString(R.string.version_upto, getAndroidVersionString(c, endVersion)); + } + + if (endVersion == -1) + return c.getString(R.string.version_and_later, getAndroidVersionString(c, startVersion)); + + + String startver = getAndroidVersionString(c, startVersion); + + if (endVersion == startVersion) + return startver; + + return String.format("%s - %s", startver, getAndroidVersionString(c, endVersion)); + } + + + private String getAndroidVersionString(Context c, int versionCode) { + switch (versionCode) { + case Build.VERSION_CODES.ICE_CREAM_SANDWICH: + return "4.0 (Ice Cream Sandwich)"; + case -441: + return "4.4.1 (KitKat)"; + case -442: + return "4.4.2 (KitKat)"; + case Build.VERSION_CODES.JELLY_BEAN_MR2: + return "4.3 (Jelly Bean MR2)"; + case Build.VERSION_CODES.KITKAT: + return "4.4 (KitKat)"; + case Build.VERSION_CODES.LOLLIPOP: + return "5.0 (Lollipop)"; + default: + return "API " + versionCode; + } + } + + + } + + private static FAQEntry[] faqitemsVersionSpecific = { + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_howto_title, R.string.faq_howto), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_killswitch_title, R.string.faq_killswitch), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_remote_api_title, R.string.faq_remote_api), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.weakmd_title, R.string.weakmd), + new FAQEntry(Build.VERSION_CODES.LOLLIPOP, -1, R.string.samsung_broken_title, R.string.samsung_broken), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_duplicate_notification_title, R.string.faq_duplicate_notification), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_androids_clients_title, R.string.faq_android_clients), + + + new FAQEntry(Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1, R.string.ab_lollipop_reinstall_title, R.string.ab_lollipop_reinstall), + + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.vpn_tethering_title, R.string.faq_tethering), + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.broken_images, R.string.broken_images_faq), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.battery_consumption_title, R.string.baterry_consumption), + + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, Build.VERSION_CODES.KITKAT, R.string.faq_system_dialogs_title, R.string.faq_system_dialogs), + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.faq_tap_mode), + + new FAQEntry(Build.VERSION_CODES.JELLY_BEAN_MR2, Build.VERSION_CODES.JELLY_BEAN_MR2, R.string.ab_secondary_users_title, R.string.ab_secondary_users), + new FAQEntry(Build.VERSION_CODES.JELLY_BEAN_MR2, -1, R.string.faq_vpndialog43_title, R.string.faq_vpndialog43), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tls_cipher_alert_title, R.string.tls_cipher_alert), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_security_title, R.string.faq_security), + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.faq_shortcut, R.string.faq_howto_shortcut), + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.tap_faq2), + + new FAQEntry(Build.VERSION_CODES.KITKAT, -1, R.string.vpn_tethering_title, R.string.ab_tethering_44), + new FAQEntry(Build.VERSION_CODES.KITKAT, -441, R.string.ab_kitkat_mss_title, R.string.ab_kitkat_mss), + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.copying_log_entries, R.string.faq_copying), + + new FAQEntry(Build.VERSION_CODES.KITKAT, -442, R.string.ab_persist_tun_title, R.string.ab_persist_tun), + new FAQEntry(Build.VERSION_CODES.KITKAT, -1, R.string.faq_routing_title, R.string.faq_routing), + new FAQEntry(Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.KITKAT, R.string.ab_kitkat_reconnect_title, R.string.ab_kitkat_reconnect), + new FAQEntry(Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.KITKAT, R.string.ab_vpn_reachability_44_title, R.string.ab_vpn_reachability_44), + + + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.ab_only_cidr_title, R.string.ab_only_cidr), + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.ab_proxy_title, R.string.ab_proxy), + + new FAQEntry(Build.VERSION_CODES.LOLLIPOP, -1, R.string.ab_not_route_to_vpn_title, R.string.ab_not_route_to_vpn), + new FAQEntry(Build.VERSION_CODES.ICE_CREAM_SANDWICH, -1, R.string.tap_mode, R.string.tap_faq3), + + // DNS weirdness in Samsung 5.0: https://plus.google.com/117315704597472009168/posts/g78bZLWmqgD + }; + + + private RecyclerView mRecyclerView; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.faq, container, false); + + DisplayMetrics displaymetrics = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); + int dpWidth = (int) (displaymetrics.widthPixels / getResources().getDisplayMetrics().density); + + //better way but does not work on 5.0 + //int dpWidth = (int) (container.getWidth()/getResources().getDisplayMetrics().density); + int columns = dpWidth / 360; + columns = Math.max(1, columns); + + + mRecyclerView = (RecyclerView) v.findViewById(R.id.faq_recycler_view); + + // use this setting to improve performance if you know that changes + // in content do not change the layout size of the RecyclerView + mRecyclerView.setHasFixedSize(true); + + + mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(columns, StaggeredGridLayoutManager.VERTICAL)); + + mRecyclerView.setAdapter(new FaqViewAdapter(getActivity(), getFAQEntries())); + + return v; + } + + private FAQEntry[] getFAQEntries() { + Vector<FAQEntry> faqItems = new Vector<>(); + + for (FAQEntry fe : faqitemsVersionSpecific) { + if (fe.runningVersion()) + faqItems.add(fe); + } + for (FAQEntry fe : faqitemsVersionSpecific) { + if (!fe.runningVersion()) + faqItems.add(fe); + } + + return faqItems.toArray(new FAQEntry[faqItems.size()]); + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java new file mode 100644 index 00000000..0be9f4a2 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.content.Context; +import android.os.AsyncTask; +import android.support.v7.widget.CardView; +import android.support.v7.widget.RecyclerView; +import android.text.Html; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import de.blinkt.openvpn.R; + +public class FaqViewAdapter extends RecyclerView.Adapter<FaqViewAdapter.FaqViewHolder> { + private final FaqFragment.FAQEntry[] mFaqItems; + private final Spanned[] mHtmlEntries; + private final Spanned[] mHtmlEntriesTitle; + private final Context mContext; + private boolean loaded =false; + + public FaqViewAdapter(Context context, FaqFragment.FAQEntry[] faqItems) { + mFaqItems = faqItems; + mContext = context; + + mHtmlEntries = new Spanned[faqItems.length]; + mHtmlEntriesTitle = new Spanned[faqItems.length]; + + new FetchStrings().execute(faqItems); + + } + + private class FetchStrings extends AsyncTask<FaqFragment.FAQEntry,Void,Void> { + + @Override + protected void onPostExecute(Void aVoid) { + loaded=true; + notifyDataSetChanged(); + } + + @Override + protected Void doInBackground(FaqFragment.FAQEntry... params) { + fetchStrings(params); + return null; + } + } + + private void fetchStrings(FaqFragment.FAQEntry[] faqItems) { + for (int i =0; i < faqItems.length; i++) { + String versionText = mFaqItems[i].getVersionsString(mContext); + String title; + String textColor=""; + + if (mFaqItems[i].title==-1) + title =""; + else + title = mContext.getString(faqItems[i].title); + + + if (!mFaqItems[i].runningVersion()) + textColor= "<font color=\"gray\">"; + + if (versionText != null) { + + mHtmlEntriesTitle[i] = (Spanned) TextUtils.concat(Html.fromHtml(textColor + title), + Html.fromHtml(textColor + "<br><small>" + versionText + "</small>")); + } else { + mHtmlEntriesTitle[i] = Html.fromHtml(title); + } + + String content = mContext.getString(faqItems[i].description); + mHtmlEntries[i] = Html.fromHtml(textColor + content); + + // Add hack R.string.faq_system_dialogs_title -> R.string.faq_system_dialog_xposed + if (faqItems[i].title == R.string.faq_system_dialogs_title) + { + Spanned xPosedtext = Html.fromHtml(textColor + mContext.getString(R.string.faq_system_dialog_xposed)); + mHtmlEntries[i] = (Spanned) TextUtils.concat(mHtmlEntries[i], xPosedtext); + } + } + } + + public static class FaqViewHolder extends RecyclerView.ViewHolder { + + private final CardView mView; + private final TextView mBody; + private final TextView mHead; + + public FaqViewHolder(View itemView) { + super(itemView); + + mView = (CardView) itemView; + mBody = (TextView)mView.findViewById(R.id.faq_body); + mHead = (TextView)mView.findViewById(R.id.faq_head); + mBody.setMovementMethod(LinkMovementMethod.getInstance()); + } + } + + @Override + public FaqViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { + View view = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.faqcard, viewGroup, false); + return new FaqViewHolder(view); + } + + @Override + public void onBindViewHolder(FaqViewHolder faqViewHolder, int i) { + + faqViewHolder.mHead.setText(mHtmlEntriesTitle[i]); + faqViewHolder.mBody.setText(mHtmlEntries[i]); + + + } + + @Override + public int getItemCount() { + if(loaded) + return mFaqItems.length; + else + return 0; + } + + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java new file mode 100644 index 00000000..9d12b83d --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.ListFragment; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.ListView; +import android.widget.SimpleAdapter; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.TreeMap; +import java.util.Vector; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.activities.FileSelect; + +public class FileSelectionFragment extends ListFragment { + + private static final String ITEM_KEY = "key"; + private static final String ITEM_IMAGE = "image"; + private static final String ROOT = "/"; + + + private List<String> path = null; + private TextView myPath; + private ArrayList<HashMap<String, Object>> mList; + + private Button selectButton; + + + private String parentPath; + private String currentPath = Environment.getExternalStorageDirectory().getAbsolutePath(); + + + private String[] formatFilter = null; + + private File selectedFile; + private HashMap<String, Integer> lastPositions = new HashMap<>(); + private String mStartPath; + private CheckBox mInlineImport; + private Button mClearButton; + private boolean mHideImport = false; + + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { + onListItemClick(getListView(), view, position, id); + onFileSelectionClick(); + return true; + } + } + + ); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.file_dialog_main, container, false); + + myPath = (TextView) v.findViewById(R.id.path); + + mInlineImport = (CheckBox) v.findViewById(R.id.doinline); + + if (mHideImport) { + mInlineImport.setVisibility(View.GONE); + mInlineImport.setChecked(false); + } + + + selectButton = (Button) v.findViewById(R.id.fdButtonSelect); + selectButton.setEnabled(false); + selectButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + onFileSelectionClick(); + } + }); + + mClearButton = (Button) v.findViewById(R.id.fdClear); + mClearButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + ((FileSelect) getActivity()).clearData(); + } + }); + if (!((FileSelect) getActivity()).showClear()) { + mClearButton.setVisibility(View.GONE); + mClearButton.setEnabled(false); + } + + return v; + } + + private void onFileSelectionClick() { + if (selectedFile != null) { + if (mInlineImport.isChecked()) + + ((FileSelect) getActivity()).importFile(selectedFile.getPath()); + else + ((FileSelect) getActivity()).setFile(selectedFile.getPath()); + } + } + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mStartPath = ((FileSelect) getActivity()).getSelectPath(); + getDir(mStartPath); + } + + public void refresh() { + getDir(Environment.getExternalStorageDirectory().getAbsolutePath()); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + + private void getDir(String dirPath) { + + boolean useAutoSelection = dirPath.length() < currentPath.length(); + + Integer position = lastPositions.get(parentPath); + + getDirImpl(dirPath); + + if (position != null && useAutoSelection) { + getListView().setSelection(position); + } + + } + + private void getDirImpl(final String dirPath) { + + currentPath = dirPath; + + final List<String> item = new ArrayList<String>(); + path = new ArrayList<String>(); + mList = new ArrayList<HashMap<String, Object>>(); + + File f = new File(currentPath); + File[] files = f.listFiles(); + if (files == null) { + currentPath = ROOT; + f = new File(currentPath); + files = f.listFiles(); + + if (files == null) + files = new File[]{}; + } + + myPath.setText(getText(R.string.location) + ": " + currentPath); + + if (!currentPath.equals(ROOT)) { + + item.add(ROOT); + addItem(ROOT, R.drawable.ic_root_folder_am); + path.add(ROOT); + + item.add("../"); + addItem("../", R.drawable.ic_root_folder_am); + path.add(f.getParent()); + parentPath = f.getParent(); + + } + + + TreeMap<String, String> dirsMap = new TreeMap<>(); + TreeMap<String, String> dirsPathMap = new TreeMap<>(); + TreeMap<String, String> filesMap = new TreeMap<>(); + TreeMap<String, String> filesPathMap = new TreeMap<>(); + + // add default locations + for (String dir: getExternalStorages()) { + // You got to love the P8 Lite to have null in this list .... + if (dir!=null) { + dirsMap.put(dir, dir); + dirsPathMap.put(dir, dir); + } + } + + for (File file : files) { + if (file.isDirectory()) { + String dirName = file.getName(); + dirsMap.put(dirName, dirName); + dirsPathMap.put(dirName, file.getPath()); + } else { + final String fileName = file.getName(); + final String fileNameLwr = fileName.toLowerCase(Locale.getDefault()); + + if (formatFilter != null) { + boolean contains = false; + for (String aFormatFilter : formatFilter) { + final String formatLwr = aFormatFilter.toLowerCase(Locale.getDefault()); + if (fileNameLwr.endsWith(formatLwr)) { + contains = true; + break; + } + } + if (contains) { + filesMap.put(fileName, fileName); + filesPathMap.put(fileName, file.getPath()); + } + } else { + filesMap.put(fileName, fileName); + filesPathMap.put(fileName, file.getPath()); + } + } + } + item.addAll(dirsMap.tailMap("").values()); + item.addAll(filesMap.tailMap("").values()); + path.addAll(dirsPathMap.tailMap("").values()); + path.addAll(filesPathMap.tailMap("").values()); + + SimpleAdapter fileList = new SimpleAdapter(getActivity(), mList, R.layout.file_dialog_row, new String[]{ + ITEM_KEY, ITEM_IMAGE}, new int[]{R.id.fdrowtext, R.id.fdrowimage}); + + for (String dir : dirsMap.tailMap("").values()) { + addItem(dir, R.drawable.ic_root_folder_am); + } + + for (String file : filesMap.tailMap("").values()) { + addItem(file, R.drawable.ic_doc_generic_am); + } + + fileList.notifyDataSetChanged(); + + setListAdapter(fileList); + + } + + private void addItem(String fileName, int imageId) { + HashMap<String, Object> item = new HashMap<String, Object>(); + item.put(ITEM_KEY, fileName); + item.put(ITEM_IMAGE, imageId); + mList.add(item); + } + + private Collection<String> getExternalStorages() { + Vector<String> dirs = new Vector<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + for (File d : getActivity().getExternalFilesDirs(null)) + dirs.add(getRootOfInnerSdCardFolder(d)); + } else { + dirs.add(Environment.getExternalStorageDirectory().getAbsolutePath()); + } + return dirs; + } + + private static String getRootOfInnerSdCardFolder(File file) { + if (file == null) + return null; + final long totalSpace = file.getTotalSpace(); + while (true) { + final File parentFile = file.getParentFile(); + if (parentFile == null || parentFile.getTotalSpace() != totalSpace + || file.equals(Environment.getExternalStorageDirectory())) + return file.getAbsolutePath(); + file = parentFile; + } + } + + + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + + File file = new File(path.get(position)); + + if (file.isDirectory()) { + selectButton.setEnabled(false); + + if (file.canRead()) { + lastPositions.put(currentPath, position); + getDir(path.get(position)); + } else { + Toast.makeText(getActivity(), + "[" + file.getName() + "] " + getActivity().getText(R.string.cant_read_folder), + Toast.LENGTH_SHORT).show(); + } + } else { + selectedFile = file; + v.setSelected(true); + selectButton.setEnabled(true); + } + } + + public void setNoInLine() { + mHideImport = true; + + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java b/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java new file mode 100644 index 00000000..34d37823 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; +import java.io.File; +import java.util.Collection; + +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceCategory; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; + +import de.blinkt.openvpn.BuildConfig; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.activities.OpenSSLSpeed; +import de.blinkt.openvpn.api.ExternalAppDatabase; +import de.blinkt.openvpn.core.ProfileManager; + + +public class GeneralSettings extends PreferenceFragment implements OnPreferenceClickListener, OnClickListener, Preference.OnPreferenceChangeListener { + + private ExternalAppDatabase mExtapp; + private ListPreference mAlwaysOnVPN; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.general_settings); + + + PreferenceCategory devHacks = (PreferenceCategory) findPreference("device_hacks"); + mAlwaysOnVPN = (ListPreference) findPreference("alwaysOnVpn"); + mAlwaysOnVPN.setOnPreferenceChangeListener(this); + + + Preference loadtun = findPreference("loadTunModule"); + if(!isTunModuleAvailable()) { + loadtun.setEnabled(false); + devHacks.removePreference(loadtun); + } + + CheckBoxPreference cm9hack = (CheckBoxPreference) findPreference("useCM9Fix"); + if (!cm9hack.isChecked() && (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1)) { + devHacks.removePreference(cm9hack); + } + + CheckBoxPreference useInternalFS = (CheckBoxPreference) findPreference("useInternalFileSelector"); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) + { + devHacks.removePreference(useInternalFS); + } + + /* Android P does not allow access to the file storage anymore */ + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) + { + Preference useInternalFileSelector = findPreference("useInternalFileSelector"); + devHacks.removePreference(useInternalFileSelector); + } + + mExtapp = new ExternalAppDatabase(getActivity()); + Preference clearapi = findPreference("clearapi"); + clearapi.setOnPreferenceClickListener(this); + + findPreference("osslspeed").setOnPreferenceClickListener(this); + + if(devHacks.getPreferenceCount()==0) + getPreferenceScreen().removePreference(devHacks); + + if (!BuildConfig.openvpn3) { + PreferenceCategory appBehaviour = (PreferenceCategory) findPreference("app_behaviour"); + CheckBoxPreference ovpn3 = (CheckBoxPreference) findPreference("ovpn3"); + ovpn3.setEnabled(false); + ovpn3.setChecked(false); + } + + + setClearApiSummary(); + } + + @Override + public void onResume() { + super.onResume(); + + + + + VpnProfile vpn = ProfileManager.getAlwaysOnVPN(getActivity()); + StringBuffer sb = new StringBuffer(getString(R.string.defaultvpnsummary)); + sb.append('\n'); + if (vpn== null) + sb.append(getString(R.string.novpn_selected)); + else + sb.append(getString(R.string.vpnselected, vpn.getName())); + mAlwaysOnVPN.setSummary(sb.toString()); + + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference== mAlwaysOnVPN) { + VpnProfile vpn = ProfileManager.get(getActivity(), (String) newValue); + mAlwaysOnVPN.setSummary(vpn.getName()); + } + return true; + } + + private void setClearApiSummary() { + Preference clearapi = findPreference("clearapi"); + + if(mExtapp.getExtAppList().isEmpty()) { + clearapi.setEnabled(false); + clearapi.setSummary(R.string.no_external_app_allowed); + } else { + clearapi.setEnabled(true); + clearapi.setSummary(getString(R.string.allowed_apps, getExtAppList(", "))); + } + } + + private String getExtAppList(String delim) { + ApplicationInfo app; + PackageManager pm = getActivity().getPackageManager(); + + StringBuilder applist = new StringBuilder(); + for (String packagename : mExtapp.getExtAppList()) { + try { + app = pm.getApplicationInfo(packagename, 0); + if (applist.length() != 0) + applist.append(delim); + applist.append(app.loadLabel(pm)); + + } catch (NameNotFoundException e) { + // App not found. Remove it from the list + mExtapp.removeApp(packagename); + } + } + + return applist.toString(); + } + + private boolean isTunModuleAvailable() { + // Check if the tun module exists on the file system + return new File("/system/lib/modules/tun.ko").length() > 10; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if(preference.getKey().equals("clearapi")){ + Builder builder = new AlertDialog.Builder(getActivity()); + builder.setPositiveButton(R.string.clear, this); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setMessage(getString(R.string.clearappsdialog,getExtAppList("\n"))); + builder.show(); + } else if (preference.getKey().equals("osslspeed")) { + startActivity(new Intent(getActivity(), OpenSSLSpeed.class)); + } + + return true; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if( which == Dialog.BUTTON_POSITIVE){ + mExtapp.clearAllApiApps(); + setClearApiSummary(); + } + } + + + +}
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java new file mode 100644 index 00000000..10c09461 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2012-2017 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Fragment; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ListView; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.core.OpenVPNManagement; +import de.blinkt.openvpn.core.TrafficHistory; +import de.blinkt.openvpn.core.VpnStatus; + +import static android.content.Context.MODE_PRIVATE; +import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount; +import static java.lang.Math.max; + +/** + * Created by arne on 19.05.17. + */ + +public class GraphFragment extends Fragment implements VpnStatus.ByteCountListener { + private static final String PREF_USE_LOG = "useLogGraph"; + private ListView mListView; + + private ChartDataAdapter mChartAdapter; + private int mColourIn; + private int mColourOut; + private int mColourPoint; + + private long firstTs; + private TextView mSpeedStatus; + private boolean mLogScale; + + private static final int TIME_PERIOD_SECDONS = 0; + private static final int TIME_PERIOD_MINUTES = 1; + private static final int TIME_PERIOD_HOURS = 2; + private Handler mHandler; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.graph, container, false); + mListView = (ListView) v.findViewById(R.id.graph_listview); + mSpeedStatus = (TextView) v.findViewById(R.id.speedStatus); + CheckBox logScaleView = (CheckBox) v.findViewById(R.id.useLogScale); + mLogScale = getActivity().getPreferences(MODE_PRIVATE).getBoolean(PREF_USE_LOG, false); + logScaleView.setChecked(mLogScale); + + List<Integer> charts = new LinkedList<>(); + charts.add(TIME_PERIOD_SECDONS); + charts.add(TIME_PERIOD_MINUTES); + charts.add(TIME_PERIOD_HOURS); + + mChartAdapter = new ChartDataAdapter(getActivity(), charts); + mListView.setAdapter(mChartAdapter); + + mColourIn = getActivity().getResources().getColor(R.color.dataIn); + mColourOut = getActivity().getResources().getColor(R.color.dataOut); + mColourPoint = getActivity().getResources().getColor(android.R.color.black); + + logScaleView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mLogScale = isChecked; + mChartAdapter.notifyDataSetChanged(); + getActivity().getPreferences(MODE_PRIVATE).edit().putBoolean(PREF_USE_LOG, isChecked).apply(); + } + }); + + mHandler = new Handler(); + + return v; + + + } + + private Runnable triggerRefresh = new Runnable() { + @Override + public void run() { + mChartAdapter.notifyDataSetChanged(); + mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500); + } + }; + + @Override + public void onResume() { + super.onResume(); + + VpnStatus.addByteCountListener(this); + mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500); + } + + @Override + public void onPause() { + super.onPause(); + + mHandler.removeCallbacks(triggerRefresh); + VpnStatus.removeByteCountListener(this); + } + + @Override + public void updateByteCount(long in, long out, long diffIn, long diffOut) { + if (firstTs == 0) + firstTs = System.currentTimeMillis() / 100; + + long now = (System.currentTimeMillis() / 100) - firstTs; + int interval = OpenVPNManagement.mBytecountInterval * 10; + Resources res = getActivity().getResources(); + + final String netstat = String.format(getString(R.string.statusline_bytecount), + humanReadableByteCount(in, false, res), + humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res), + humanReadableByteCount(out, false, res), + humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res)); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mHandler.removeCallbacks(triggerRefresh); + mSpeedStatus.setText(netstat); + mChartAdapter.notifyDataSetChanged(); + mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval * 1500); + } + }); + + } + + private class ChartDataAdapter extends ArrayAdapter<Integer> { + + private Context mContext; + + public ChartDataAdapter(Context context, List<Integer> trafficData) { + super(context, 0, trafficData); + mContext = context; + } + + @NonNull + @Override + public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) { + ViewHolder holder = null; + + if (convertView == null) { + + holder = new ViewHolder(); + + convertView = LayoutInflater.from(mContext).inflate( + R.layout.graph_item, parent, false); + holder.chart = (LineChart) convertView.findViewById(R.id.chart); + holder.title = (TextView) convertView.findViewById(R.id.tvName); + convertView.setTag(holder); + + } else { + holder = (ViewHolder) convertView.getTag(); + } + + // apply styling + // holder.chart.setValueTypeface(mTf); + holder.chart.getDescription().setEnabled(false); + holder.chart.setDrawGridBackground(false); + + XAxis xAxis = holder.chart.getXAxis(); + xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); + xAxis.setDrawGridLines(false); + xAxis.setDrawAxisLine(true); + + switch (position) { + case TIME_PERIOD_HOURS: + holder.title.setText(R.string.avghour); + break; + case TIME_PERIOD_MINUTES: + holder.title.setText(R.string.avgmin); + break; + default: + holder.title.setText(R.string.last5minutes); + break; + } + + xAxis.setValueFormatter(new IAxisValueFormatter() { + + + @Override + public String getFormattedValue(float value, AxisBase axis) { + switch (position) { + case TIME_PERIOD_HOURS: + return String.format(Locale.getDefault(), "%.0f\u2009h ago", (axis.getAxisMaximum() - value) / 10 / 3600); + case TIME_PERIOD_MINUTES: + return String.format(Locale.getDefault(), "%.0f\u2009m ago", (axis.getAxisMaximum() - value) / 10 / 60); + default: + return String.format(Locale.getDefault(), "%.0f\u2009s ago", (axis.getAxisMaximum() - value) / 10); + } + + } + }); + xAxis.setLabelCount(5); + + YAxis yAxis = holder.chart.getAxisLeft(); + yAxis.setLabelCount(5, false); + + final Resources res = getActivity().getResources(); + yAxis.setValueFormatter(new IAxisValueFormatter() { + @Override + public String getFormattedValue(float value, AxisBase axis) { + if (mLogScale && value < 2.1f) + return "< 100\u2009bit/s"; + if (mLogScale) + value = (float) Math.pow(10, value) / 8; + + return humanReadableByteCount((long) value, true, res); + } + }); + + holder.chart.getAxisRight().setEnabled(false); + + LineData data = getDataSet(position); + float ymax = data.getYMax(); + + if (mLogScale) { + yAxis.setAxisMinimum(2f); + yAxis.setAxisMaximum((float) Math.ceil(ymax)); + yAxis.setLabelCount((int) (Math.ceil(ymax - 2f))); + } else { + yAxis.setAxisMinimum(0f); + yAxis.resetAxisMaximum(); + yAxis.setLabelCount(6); + } + + if (data.getDataSetByIndex(0).getEntryCount() < 3) + holder.chart.setData(null); + else + holder.chart.setData(data); + + holder.chart.setNoDataText(getString(R.string.notenoughdata)); + + holder.chart.invalidate(); + //holder.chart.animateX(750); + + return convertView; + } + + private LineData getDataSet(int timeperiod) { + + LinkedList<Entry> dataIn = new LinkedList<>(); + LinkedList<Entry> dataOut = new LinkedList<>(); + + long interval; + long totalInterval; + + LinkedList<TrafficHistory.TrafficDatapoint> list; + switch (timeperiod) { + case TIME_PERIOD_HOURS: + list = VpnStatus.trafficHistory.getHours(); + interval = TrafficHistory.TIME_PERIOD_HOURS; + totalInterval = 0; + break; + case TIME_PERIOD_MINUTES: + list = VpnStatus.trafficHistory.getMinutes(); + interval = TrafficHistory.TIME_PERIOD_MINTUES; + totalInterval = TrafficHistory.TIME_PERIOD_HOURS * TrafficHistory.PERIODS_TO_KEEP; + ; + + break; + default: + list = VpnStatus.trafficHistory.getSeconds(); + interval = OpenVPNManagement.mBytecountInterval * 1000; + totalInterval = TrafficHistory.TIME_PERIOD_MINTUES * TrafficHistory.PERIODS_TO_KEEP; + break; + } + if (list.size() == 0) { + list = TrafficHistory.getDummyList(); + } + + + long lastts = 0; + float zeroValue; + if (mLogScale) + zeroValue = 2; + else + zeroValue = 0; + + long now = System.currentTimeMillis(); + + + long firstTimestamp = 0; + long lastBytecountOut = 0; + long lastBytecountIn = 0; + + for (TrafficHistory.TrafficDatapoint tdp : list) { + if (totalInterval != 0 && (now - tdp.timestamp) > totalInterval) + continue; + + if (firstTimestamp == 0) { + firstTimestamp = list.peek().timestamp; + lastBytecountIn = list.peek().in; + lastBytecountOut = list.peek().out; + } + + float t = (tdp.timestamp - firstTimestamp) / 100f; + + float in = (tdp.in - lastBytecountIn) / (float) (interval / 1000); + float out = (tdp.out - lastBytecountOut) / (float) (interval / 1000); + + lastBytecountIn = tdp.in; + lastBytecountOut = tdp.out; + + if (mLogScale) { + in = max(2f, (float) Math.log10(in * 8)); + out = max(2f, (float) Math.log10(out * 8)); + } + + if (lastts > 0 && (tdp.timestamp - lastts > 2 * interval)) { + dataIn.add(new Entry((lastts - firstTimestamp + interval) / 100f, zeroValue)); + dataOut.add(new Entry((lastts - firstTimestamp + interval) / 100f, zeroValue)); + + dataIn.add(new Entry(t - interval / 100f, zeroValue)); + dataOut.add(new Entry(t - interval / 100f, zeroValue)); + } + + lastts = tdp.timestamp; + + dataIn.add(new Entry(t, in)); + dataOut.add(new Entry(t, out)); + + } + if (lastts < now - interval) { + + if (now - lastts > 2 * interval * 1000) { + dataIn.add(new Entry((lastts - firstTimestamp + interval * 1000) / 100f, zeroValue)); + dataOut.add(new Entry((lastts - firstTimestamp + interval * 1000) / 100f, zeroValue)); + } + + dataIn.add(new Entry((now - firstTimestamp) / 100, zeroValue)); + dataOut.add(new Entry((now - firstTimestamp) / 100, zeroValue)); + } + + ArrayList<ILineDataSet> dataSets = new ArrayList<>(); + + + LineDataSet indata = new LineDataSet(dataIn, getString(R.string.data_in)); + LineDataSet outdata = new LineDataSet(dataOut, getString(R.string.data_out)); + + setLineDataAttributes(indata, mColourIn); + setLineDataAttributes(outdata, mColourOut); + + dataSets.add(indata); + dataSets.add(outdata); + + return new LineData(dataSets); + } + + private void setLineDataAttributes(LineDataSet dataSet, int colour) { + dataSet.setLineWidth(2); + dataSet.setCircleRadius(1); + dataSet.setDrawCircles(true); + dataSet.setCircleColor(mColourPoint); + dataSet.setDrawFilled(true); + dataSet.setFillAlpha(42); + dataSet.setFillColor(colour); + dataSet.setColor(colour); + dataSet.setMode(LineDataSet.Mode.LINEAR); + + dataSet.setDrawValues(false); + } + } + + private static class ViewHolder { + LineChart chart; + TextView title; + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java b/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java new file mode 100644 index 00000000..41206a54 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import de.blinkt.openvpn.activities.FileSelect; +import de.blinkt.openvpn.R; + +public class InlineFileTab extends Fragment +{ + + private static final int MENU_SAVE = 0; + private EditText mInlineData; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mInlineData.setText(((FileSelect)getActivity()).getInlineData()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) + { + + View v = inflater.inflate(R.layout.file_dialog_inline, container, false); + mInlineData =(EditText) v.findViewById(R.id.inlineFileData); + return v; + } + + public void setData(String data) { + if(mInlineData!=null) + mInlineData.setText(data); + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(0, MENU_SAVE, 0, R.string.menu_use_inline_data) + .setIcon(android.R.drawable.ic_menu_save) + .setAlphabeticShortcut('u') + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(item.getItemId()==MENU_SAVE){ + ((FileSelect)getActivity()).saveInlineData(null, mInlineData.getText().toString()); + return true; + } + return super.onOptionsItemSelected(item); + } + +}
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt new file mode 100644 index 00000000..323b3a4d --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments + +import android.annotation.TargetApi +import android.app.Activity +import android.app.AlertDialog +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.security.KeyChain +import android.security.KeyChainException +import android.security.keystore.KeyInfo +import android.text.TextUtils +import android.view.View +import android.widget.AdapterView +import android.widget.Spinner +import android.widget.TextView +import android.widget.Toast +import de.blinkt.openvpn.R +import de.blinkt.openvpn.VpnProfile +import de.blinkt.openvpn.api.ExternalCertificateProvider +import de.blinkt.openvpn.core.ExtAuthHelper +import de.blinkt.openvpn.core.X509Utils +import java.security.KeyFactory +import java.security.PrivateKey + +import java.security.cert.X509Certificate + +internal abstract class KeyChainSettingsFragment : Settings_Fragment(), View.OnClickListener, Handler.Callback { + + + private lateinit var mAliasCertificate: TextView + private lateinit var mAliasName: TextView + private var mHandler: Handler? = null + private lateinit var mExtAliasName: TextView + private lateinit var mExtAuthSpinner: Spinner + + private val isInHardwareKeystore: Boolean + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + @Throws(KeyChainException::class, InterruptedException::class) + get() { + val key : PrivateKey = KeyChain.getPrivateKey(activity.applicationContext, mProfile.mAlias) ?: return false + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) + { + val keyFactory = KeyFactory.getInstance(key.getAlgorithm(), "AndroidKeyStore") + val keyInfo = keyFactory.getKeySpec(key, KeyInfo::class.java) + return keyInfo.isInsideSecureHardware() + + } else { + val algorithm = key.algorithm + return KeyChain.isBoundKeyAlgorithm(algorithm) + } + } + + + private fun setKeyStoreAlias() { + if (mProfile.mAlias == null) { + mAliasName.setText(R.string.client_no_certificate) + mAliasCertificate.text = "" + } else { + mAliasCertificate.text = "Loading certificate from Keystore..." + mAliasName.text = mProfile.mAlias + setCertificate(false) + } + } + + private fun setExtAlias() { + if (mProfile.mAlias == null) { + mExtAliasName.setText(R.string.extauth_not_configured) + mAliasCertificate.text = "" + } else { + mAliasCertificate.text = "Querying certificate from external provider..." + mExtAliasName.text = "" + setCertificate(true) + } + } + + private fun fetchExtCertificateMetaData() { + object : Thread() { + override fun run() { + try { + val b = ExtAuthHelper.getCertificateMetaData(activity, mProfile.mExternalAuthenticator, mProfile.mAlias) + mProfile.mAlias = b.getString(ExtAuthHelper.EXTRA_ALIAS) + activity.runOnUiThread { setAlias() } + } catch (e: KeyChainException) { + e.printStackTrace() + } + + } + }.start() + } + + + protected fun setCertificate(external: Boolean) { + object : Thread() { + override fun run() { + var certstr = "" + var metadata: Bundle? = null + try { + val cert: X509Certificate? + + if (external) { + if (!TextUtils.isEmpty(mProfile.mExternalAuthenticator) && !TextUtils.isEmpty(mProfile.mAlias)) { + cert = ExtAuthHelper.getCertificateChain(activity, mProfile.mExternalAuthenticator, mProfile.mAlias)!![0] + metadata = ExtAuthHelper.getCertificateMetaData(activity, mProfile.mExternalAuthenticator, mProfile.mAlias) + } else { + cert = null + certstr = getString(R.string.extauth_not_configured) + } + } else { + cert = KeyChain.getCertificateChain(activity.applicationContext, mProfile.mAlias)!![0] + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + run { + if (isInHardwareKeystore) + certstr += getString(R.string.hwkeychain) + } + } + } + if (cert != null) { + certstr += X509Utils.getCertificateValidityString(cert, resources) + certstr += X509Utils.getCertificateFriendlyName(cert) + } + + + } catch (e: Exception) { + certstr = "Could not get certificate from Keystore: " + e.localizedMessage!! + } + + val certStringCopy = certstr + val finalMetadata = metadata + activity.runOnUiThread { + mAliasCertificate.text = certStringCopy + if (finalMetadata != null) + mExtAliasName.text = finalMetadata.getString(ExtAuthHelper.EXTRA_DESCRIPTION) + + } + + } + }.start() + } + + protected fun initKeychainViews(v: View) { + v.findViewById<View>(R.id.select_keystore_button).setOnClickListener(this) + v.findViewById<View>(R.id.configure_extauth_button)?.setOnClickListener(this) + v.findViewById<View>(R.id.install_keystore_button).setOnClickListener(this) + mAliasCertificate = v.findViewById(R.id.alias_certificate) + mExtAuthSpinner = v.findViewById(R.id.extauth_spinner) + mExtAuthSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val selectedProvider = parent.getItemAtPosition(position) as ExtAuthHelper.ExternalAuthProvider + if (selectedProvider.packageName != mProfile.mExternalAuthenticator) { + mProfile.mAlias = "" + } + } + + override fun onNothingSelected(parent: AdapterView<*>) { + + } + } + mExtAliasName = v.findViewById(R.id.extauth_detail) + mAliasName = v.findViewById(R.id.aliasname) + if (mHandler == null) { + mHandler = Handler(this) + } + ExtAuthHelper.setExternalAuthProviderSpinnerList(mExtAuthSpinner, mProfile.mExternalAuthenticator) + + v.findViewById<View>(R.id.install_keystore_button).setOnClickListener { + startActivity(KeyChain.createInstallIntent()) }; + } + + override fun onClick(v: View) { + if (v === v.findViewById<View>(R.id.select_keystore_button)) { + showCertDialog() + } else if (v === v.findViewById<View>(R.id.configure_extauth_button)) { + startExternalAuthConfig() + } + } + + private fun startExternalAuthConfig() { + val eAuth = mExtAuthSpinner.selectedItem as ExtAuthHelper.ExternalAuthProvider + mProfile.mExternalAuthenticator = eAuth.packageName + if (!eAuth.configurable) { + fetchExtCertificateMetaData() + return + } + val extauth = Intent(ExtAuthHelper.ACTION_CERT_CONFIGURATION) + extauth.setPackage(eAuth.packageName) + extauth.putExtra(ExtAuthHelper.EXTRA_ALIAS, mProfile.mAlias) + startActivityForResult(extauth, UPDATEE_EXT_ALIAS) + } + + override fun savePreferences() { + + } + + override fun onStart() { + super.onStart() + loadPreferences() + } + + fun showCertDialog() { + try { + KeyChain.choosePrivateKeyAlias(activity, + { alias -> + // Credential alias selected. Remember the alias selection for future use. + mProfile.mAlias = alias + mHandler!!.sendEmptyMessage(UPDATE_ALIAS) + }, + arrayOf("RSA", "EC"), null, // issuer, null for any + mProfile.mServerName, // host name of server requesting the cert, null if unavailable + -1, // port of server requesting the cert, -1 if unavailable + mProfile.mAlias)// List of acceptable key types. null for any + // alias to preselect, null if unavailable + } catch (anf: ActivityNotFoundException) { + val ab = AlertDialog.Builder(activity) + ab.setTitle(R.string.broken_image_cert_title) + ab.setMessage(R.string.broken_image_cert) + ab.setPositiveButton(android.R.string.ok, null) + ab.show() + } + + } + + protected open fun loadPreferences() { + setAlias() + + } + + private fun setAlias() { + if (mProfile.mAuthenticationType == VpnProfile.TYPE_EXTERNAL_APP) + setExtAlias() + else + setKeyStoreAlias() + } + + override fun handleMessage(msg: Message): Boolean { + setAlias() + return true + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == UPDATEE_EXT_ALIAS && resultCode == Activity.RESULT_OK) { + mProfile.mAlias = data.getStringExtra(ExtAuthHelper.EXTRA_ALIAS) + mExtAliasName.text = data.getStringExtra(ExtAuthHelper.EXTRA_DESCRIPTION) + } + } + + companion object { + private val UPDATE_ALIAS = 20 + private val UPDATEE_EXT_ALIAS = 210 + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java new file mode 100644 index 00000000..e64ce2cd --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java @@ -0,0 +1,694 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ListFragment; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Message; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.text.SpannableString; +import android.text.format.DateFormat; +import android.text.style.ImageSpan; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.RadioGroup; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; +import java.util.Vector; + +import de.blinkt.openvpn.LaunchVPN; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.activities.DisconnectVPN; +import de.blinkt.openvpn.activities.MainActivity; +import de.blinkt.openvpn.activities.VPNPreferences; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.OpenVPNManagement; +import de.blinkt.openvpn.core.OpenVPNService; +import de.blinkt.openvpn.core.Preferences; +import de.blinkt.openvpn.core.ProfileManager; +import de.blinkt.openvpn.core.VpnStatus; +import de.blinkt.openvpn.core.LogItem; +import de.blinkt.openvpn.core.VpnStatus.LogListener; +import de.blinkt.openvpn.core.VpnStatus.StateListener; + +import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount; + +public class LogFragment extends ListFragment implements StateListener, SeekBar.OnSeekBarChangeListener, RadioGroup.OnCheckedChangeListener, VpnStatus.ByteCountListener { + private static final String LOGTIMEFORMAT = "logtimeformat"; + private static final int START_VPN_CONFIG = 0; + private static final String VERBOSITYLEVEL = "verbositylevel"; + + + + private SeekBar mLogLevelSlider; + private LinearLayout mOptionsLayout; + private RadioGroup mTimeRadioGroup; + private TextView mUpStatus; + private TextView mDownStatus; + private TextView mConnectStatus; + private boolean mShowOptionsLayout; + private CheckBox mClearLogCheckBox; + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + ladapter.setLogLevel(progress + 1); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + switch (checkedId) { + case R.id.radioISO: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO); + break; + case R.id.radioNone: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE); + break; + case R.id.radioShort: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT); + break; + + } + } + + @Override + public void updateByteCount(long in, long out, long diffIn, long diffOut) { + //%2$s/s %1$s - ↑%4$s/s %3$s + Resources res = getActivity().getResources(); + final String down = String.format("%2$s %1$s", humanReadableByteCount(in, false, res), humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res)); + final String up = String.format("%2$s %1$s", humanReadableByteCount(out, false, res), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res)); + + if (mUpStatus != null && mDownStatus != null) { + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mUpStatus.setText(up); + mDownStatus.setText(down); + } + }); + } + } + + } + + + class LogWindowListAdapter implements ListAdapter, LogListener, Callback { + + private static final int MESSAGE_NEWLOG = 0; + + private static final int MESSAGE_CLEARLOG = 1; + + private static final int MESSAGE_NEWTS = 2; + private static final int MESSAGE_NEWLOGLEVEL = 3; + + public static final int TIME_FORMAT_NONE = 0; + public static final int TIME_FORMAT_SHORT = 1; + public static final int TIME_FORMAT_ISO = 2; + private static final int MAX_STORED_LOG_ENTRIES = 1000; + + private Vector<LogItem> allEntries = new Vector<>(); + + private Vector<LogItem> currentLevelEntries = new Vector<LogItem>(); + + private Handler mHandler; + + private Vector<DataSetObserver> observers = new Vector<DataSetObserver>(); + + private int mTimeFormat = 0; + private int mLogLevel = 3; + + + public LogWindowListAdapter() { + initLogBuffer(); + if (mHandler == null) { + mHandler = new Handler(this); + } + + VpnStatus.addLogListener(this); + } + + + private void initLogBuffer() { + allEntries.clear(); + Collections.addAll(allEntries, VpnStatus.getlogbuffer()); + initCurrentMessages(); + } + + String getLogStr() { + String str = ""; + for (LogItem entry : allEntries) { + str += getTime(entry, TIME_FORMAT_ISO) + entry.getString(getActivity()) + '\n'; + } + return str; + } + + + private void shareLog() { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr()); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.ics_openvpn_log_file)); + shareIntent.setType("text/plain"); + startActivity(Intent.createChooser(shareIntent, "Send Logfile")); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + observers.add(observer); + + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + observers.remove(observer); + } + + @Override + public int getCount() { + return currentLevelEntries.size(); + } + + @Override + public Object getItem(int position) { + return currentLevelEntries.get(position); + } + + @Override + public long getItemId(int position) { + return ((Object) currentLevelEntries.get(position)).hashCode(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView v; + if (convertView == null) + v = new TextView(getActivity()); + else + v = (TextView) convertView; + + LogItem le = currentLevelEntries.get(position); + String msg = le.getString(getActivity()); + String time = getTime(le, mTimeFormat); + msg = time + msg; + + int spanStart = time.length(); + + SpannableString t = new SpannableString(msg); + + //t.setSpan(getSpanImage(le,(int)v.getTextSize()),spanStart,spanStart+1, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + v.setText(t); + return v; + } + + private String getTime(LogItem le, int time) { + if (time != TIME_FORMAT_NONE) { + Date d = new Date(le.getLogtime()); + java.text.DateFormat timeformat; + if (time == TIME_FORMAT_ISO) + timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + else + timeformat = DateFormat.getTimeFormat(getActivity()); + + return timeformat.format(d) + " "; + + } else { + return ""; + } + + } + + private ImageSpan getSpanImage(LogItem li, int imageSize) { + int imageRes = android.R.drawable.ic_menu_call; + + switch (li.getLogLevel()) { + case ERROR: + imageRes = android.R.drawable.ic_notification_clear_all; + break; + case INFO: + imageRes = android.R.drawable.ic_menu_compass; + break; + case VERBOSE: + imageRes = android.R.drawable.ic_menu_info_details; + break; + case WARNING: + imageRes = android.R.drawable.ic_menu_camera; + break; + } + + Drawable d = getResources().getDrawable(imageRes); + + + //d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); + d.setBounds(0, 0, imageSize, imageSize); + ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM); + + return span; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return currentLevelEntries.isEmpty(); + + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public void newLog(LogItem logMessage) { + Message msg = Message.obtain(); + assert (msg != null); + msg.what = MESSAGE_NEWLOG; + Bundle bundle = new Bundle(); + bundle.putParcelable("logmessage", logMessage); + msg.setData(bundle); + mHandler.sendMessage(msg); + } + + @Override + public boolean handleMessage(Message msg) { + // We have been called + if (msg.what == MESSAGE_NEWLOG) { + + LogItem logMessage = msg.getData().getParcelable("logmessage"); + if (addLogMessage(logMessage)) + for (DataSetObserver observer : observers) { + observer.onChanged(); + } + } else if (msg.what == MESSAGE_CLEARLOG) { + for (DataSetObserver observer : observers) { + observer.onInvalidated(); + } + initLogBuffer(); + } else if (msg.what == MESSAGE_NEWTS) { + for (DataSetObserver observer : observers) { + observer.onInvalidated(); + } + } else if (msg.what == MESSAGE_NEWLOGLEVEL) { + initCurrentMessages(); + + for (DataSetObserver observer : observers) { + observer.onChanged(); + } + + } + + return true; + } + + private void initCurrentMessages() { + currentLevelEntries.clear(); + for (LogItem li : allEntries) { + if (li.getVerbosityLevel() <= mLogLevel || + mLogLevel == VpnProfile.MAXLOGLEVEL) + currentLevelEntries.add(li); + } + } + + /** + * @param logmessage + * @return True if the current entries have changed + */ + private boolean addLogMessage(LogItem logmessage) { + allEntries.add(logmessage); + + if (allEntries.size() > MAX_STORED_LOG_ENTRIES) { + Vector<LogItem> oldAllEntries = allEntries; + allEntries = new Vector<LogItem>(allEntries.size()); + for (int i = 50; i < oldAllEntries.size(); i++) { + allEntries.add(oldAllEntries.elementAt(i)); + } + initCurrentMessages(); + return true; + } else { + if (logmessage.getVerbosityLevel() <= mLogLevel) { + currentLevelEntries.add(logmessage); + return true; + } else { + return false; + } + } + } + + void clearLog() { + // Actually is probably called from GUI Thread as result of the user + // pressing a button. But better safe than sorry + VpnStatus.clearLog(); + VpnStatus.logInfo(R.string.logCleared); + mHandler.sendEmptyMessage(MESSAGE_CLEARLOG); + } + + + public void setTimeFormat(int newTimeFormat) { + mTimeFormat = newTimeFormat; + mHandler.sendEmptyMessage(MESSAGE_NEWTS); + } + + public void setLogLevel(int logLevel) { + mLogLevel = logLevel; + mHandler.sendEmptyMessage(MESSAGE_NEWLOGLEVEL); + } + + } + + + private LogWindowListAdapter ladapter; + private TextView mSpeedView; + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.clearlog) { + ladapter.clearLog(); + return true; + } else if (item.getItemId() == R.id.cancel) { + Intent intent = new Intent(getActivity(), DisconnectVPN.class); + startActivity(intent); + return true; + } else if (item.getItemId() == R.id.send) { + ladapter.shareLog(); + } else if (item.getItemId() == R.id.edit_vpn) { + VpnProfile lastConnectedprofile = ProfileManager.get(getActivity(), VpnStatus.getLastConnectedVPNProfile()); + + if (lastConnectedprofile != null) { + Intent vprefintent = new Intent(getActivity(), VPNPreferences.class) + .putExtra(VpnProfile.EXTRA_PROFILEUUID, lastConnectedprofile.getUUIDString()); + startActivityForResult(vprefintent, START_VPN_CONFIG); + } else { + Toast.makeText(getActivity(), R.string.log_no_last_vpn, Toast.LENGTH_LONG).show(); + } + } else if (item.getItemId() == R.id.toggle_time) { + showHideOptionsPanel(); + } else if (item.getItemId() == android.R.id.home) { + // This is called when the Home (Up) button is pressed + // in the Action Bar. + Intent parentActivityIntent = new Intent(getActivity(), MainActivity.class); + parentActivityIntent.addFlags( + Intent.FLAG_ACTIVITY_CLEAR_TOP | + Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(parentActivityIntent); + getActivity().finish(); + return true; + + } + return super.onOptionsItemSelected(item); + + } + + private void showHideOptionsPanel() { + boolean optionsVisible = (mOptionsLayout.getVisibility() != View.GONE); + + ObjectAnimator anim; + if (optionsVisible) { + anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 1.0f, 0f); + anim.addListener(collapseListener); + + } else { + mOptionsLayout.setVisibility(View.VISIBLE); + anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 0f, 1.0f); + //anim = new TranslateAnimation(0.0f, 0.0f, mOptionsLayout.getHeight(), 0.0f); + + } + + //anim.setInterpolator(new AccelerateInterpolator(1.0f)); + //anim.setDuration(300); + //mOptionsLayout.startAnimation(anim); + anim.start(); + + } + + AnimatorListenerAdapter collapseListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mOptionsLayout.setVisibility(View.GONE); + } + + }; + + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.logmenu, menu); + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) + menu.removeItem(R.id.toggle_time); + } + + + @Override + public void onResume() { + super.onResume(); + Intent intent = new Intent(getActivity(), OpenVPNService.class); + intent.setAction(OpenVPNService.START_SERVICE); + } + + @Override + public void onStart() { + super.onStart(); + VpnStatus.addStateListener(this); + VpnStatus.addByteCountListener(this); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == START_VPN_CONFIG && resultCode == Activity.RESULT_OK) { + String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID); + + final VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN); + ProfileManager.getInstance(getActivity()).saveProfile(getActivity(), profile); + // Name could be modified, reset List adapter + + AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity()); + dialog.setTitle(R.string.configuration_changed); + dialog.setMessage(R.string.restart_vpn_after_change); + + + dialog.setPositiveButton(R.string.restart, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(getActivity(), LaunchVPN.class); + intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUIDString()); + intent.setAction(Intent.ACTION_MAIN); + startActivity(intent); + } + + + }); + dialog.setNegativeButton(R.string.ignore, null); + dialog.create().show(); + } + super.onActivityResult(requestCode, resultCode, data); + } + + + @Override + public void onStop() { + super.onStop(); + VpnStatus.removeStateListener(this); + VpnStatus.removeByteCountListener(this); + + getActivity().getPreferences(0).edit().putInt(LOGTIMEFORMAT, ladapter.mTimeFormat) + .putInt(VERBOSITYLEVEL, ladapter.mLogLevel).apply(); + + } + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ListView lv = getListView(); + + lv.setOnItemLongClickListener(new OnItemLongClickListener() { + + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, + int position, long id) { + ClipboardManager clipboard = (ClipboardManager) + getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Log Entry", ((TextView) view).getText()); + clipboard.setPrimaryClip(clip); + Toast.makeText(getActivity(), R.string.copied_entry, Toast.LENGTH_SHORT).show(); + return true; + } + }); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.log_fragment, container, false); + + setHasOptionsMenu(true); + + ladapter = new LogWindowListAdapter(); + ladapter.mTimeFormat = getActivity().getPreferences(0).getInt(LOGTIMEFORMAT, 1); + int logLevel = getActivity().getPreferences(0).getInt(VERBOSITYLEVEL, 1); + ladapter.setLogLevel(logLevel); + + setListAdapter(ladapter); + + mTimeRadioGroup = (RadioGroup) v.findViewById(R.id.timeFormatRadioGroup); + mTimeRadioGroup.setOnCheckedChangeListener(this); + + if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_ISO) { + mTimeRadioGroup.check(R.id.radioISO); + } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_NONE) { + mTimeRadioGroup.check(R.id.radioNone); + } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_SHORT) { + mTimeRadioGroup.check(R.id.radioShort); + } + + mClearLogCheckBox = (CheckBox) v.findViewById(R.id.clearlogconnect); + mClearLogCheckBox.setChecked(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(LaunchVPN.CLEARLOG, true)); + mClearLogCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Preferences.getDefaultSharedPreferences(getActivity()).edit().putBoolean(LaunchVPN.CLEARLOG, isChecked).apply(); + } + }); + + mSpeedView = (TextView) v.findViewById(R.id.speed); + + mOptionsLayout = (LinearLayout) v.findViewById(R.id.logOptionsLayout); + mLogLevelSlider = (SeekBar) v.findViewById(R.id.LogLevelSlider); + mLogLevelSlider.setMax(VpnProfile.MAXLOGLEVEL - 1); + mLogLevelSlider.setProgress(logLevel - 1); + + mLogLevelSlider.setOnSeekBarChangeListener(this); + + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) + mOptionsLayout.setVisibility(View.VISIBLE); + + mUpStatus = (TextView) v.findViewById(R.id.speedUp); + mDownStatus = (TextView) v.findViewById(R.id.speedDown); + mConnectStatus = (TextView) v.findViewById(R.id.speedStatus); + if (mShowOptionsLayout) + mOptionsLayout.setVisibility(View.VISIBLE); + return v; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // Scroll to the end of the list end + //getListView().setSelection(getListView().getAdapter().getCount()-1); + } + + @Override + public void onAttach(Context activity) { + super.onAttach(activity); + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) { + mShowOptionsLayout = true; + if (mOptionsLayout != null) + mOptionsLayout.setVisibility(View.VISIBLE); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + //getActionBar().setDisplayHomeAsUpEnabled(true); + + } + + + @Override + public void updateState(final String status, final String logMessage, final int resId, final ConnectionStatus level) { + if (isAdded()) { + final String cleanLogMessage = VpnStatus.getLastCleanLogMessage(getActivity()); + + getActivity().runOnUiThread(new Runnable() { + + @Override + public void run() { + if (isAdded()) { + if (mSpeedView != null) { + mSpeedView.setText(cleanLogMessage); + } + if (mConnectStatus != null) + mConnectStatus.setText(cleanLogMessage); + } + } + }); + } + } + + @Override + public void setConnectedVPN(String uuid) { + } + + + @Override + public void onDestroy() { + VpnStatus.removeLogListener(ladapter); + super.onDestroy(); + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java new file mode 100644 index 00000000..9ac8bebb --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.os.Bundle; +import android.preference.PreferenceFragment; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ProfileManager; + +public abstract class OpenVpnPreferencesFragment extends PreferenceFragment { + + protected VpnProfile mProfile; + + protected abstract void loadSettings(); + protected abstract void saveSettings(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String profileUUID = getArguments().getString(getActivity().getPackageName() + ".profileUUID"); + mProfile = ProfileManager.get(getActivity(),profileUUID); + getActivity().setTitle(getString(R.string.edit_profile_title, mProfile.getName())); + + } + + @Override + public void onPause() { + super.onPause(); + saveSettings(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if(savedInstanceState!=null) { + String profileUUID=savedInstanceState.getString(VpnProfile.EXTRA_PROFILEUUID); + mProfile = ProfileManager.get(getActivity(),profileUUID); + loadSettings(); + } + } + + @Override + public void onSaveInstanceState (Bundle outState) { + super.onSaveInstanceState(outState); + saveSettings(); + outState.putString(VpnProfile.EXTRA_PROFILEUUID, mProfile.getUUIDString()); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java new file mode 100644 index 00000000..0fe40905 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import java.io.File; +import java.util.ArrayList; +import java.util.Date; + +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Bundle; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.core.VpnStatus; + +public class SendDumpFragment extends Fragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + final View v = inflater.inflate(R.layout.fragment_senddump, container, false); + v.findViewById(R.id.senddump).setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + emailMiniDumps(); + } + }); + + new Thread(new Runnable() { + @Override + public void run() { + final Pair<File, Long> ldump = getLastestDump(getActivity()); + if (ldump==null) + return; + // Do in background since it does I/O + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + TextView dumpDateText = (TextView) v.findViewById(R.id.dumpdate); + String datestr = (new Date(ldump.second)).toString(); + long timediff = System.currentTimeMillis() - ldump.second; + long minutes = timediff / 1000 / 60 % 60; + long hours = timediff / 1000 / 60 / 60; + dumpDateText.setText(getString(R.string.lastdumpdate, hours, minutes, datestr)); + + } + }); + } + }).start(); + return v; + } + + public void emailMiniDumps() + { + //need to "send multiple" to get more than one attachment + final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE); + emailIntent.setType("*/*"); + emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, + new String[]{"Arne Schwabe <arne@rfc2549.org>"}); + + String version; + String name="ics-openvpn"; + try { + PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0); + version = packageinfo.versionName; + name = packageinfo.applicationInfo.name; + } catch (NameNotFoundException e) { + version = "error fetching version"; + } + + + emailIntent.putExtra(Intent.EXTRA_SUBJECT, String.format("%s(%s) %s Minidump",name, getActivity().getPackageName(), version)); + + emailIntent.putExtra(Intent.EXTRA_TEXT, "Please describe the issue you have experienced"); + + ArrayList<Uri> uris = new ArrayList<>(); + + Pair<File, Long> ldump = getLastestDump(getActivity()); + if(ldump==null) { + VpnStatus.logError("No Minidump found!"); + } + + uris.add(Uri.parse("content://de.blinkt.openvpn.FileProvider/" + ldump.first.getName())); + uris.add(Uri.parse("content://de.blinkt.openvpn.FileProvider/" + ldump.first.getName() + ".log")); + + emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + startActivity(emailIntent); + } + + static public Pair<File,Long> getLastestDump(Context c) { + long newestDumpTime=0; + File newestDumpFile=null; + + if (c.getCacheDir() ==null) + return null; + + for(File f:c.getCacheDir().listFiles()) { + if(!f.getName().endsWith(".dmp")) + continue; + + if (newestDumpTime < f.lastModified()) { + newestDumpTime = f.lastModified(); + newestDumpFile=f; + } + } + // Ignore old dumps + if(System.currentTimeMillis() - 48 * 60 * 1000 > newestDumpTime ) + return null; + + return Pair.create(newestDumpFile, newestDumpTime); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt new file mode 100644 index 00000000..dd2aa3b7 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments + +import android.Manifest +import android.app.Activity +import android.app.Fragment +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.BaseAdapter +import android.widget.CompoundButton +import android.widget.Filter +import android.widget.Filterable +import android.widget.ImageView +import android.widget.ListView +import android.widget.SearchView +import android.widget.Switch +import android.widget.TextView + +import java.util.Collections +import java.util.Locale +import java.util.Vector + +import de.blinkt.openvpn.R +import de.blinkt.openvpn.VpnProfile +import de.blinkt.openvpn.core.ProfileManager + +/** + * Created by arne on 16.11.14. + */ +class Settings_Allowed_Apps : Fragment(), AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener { + private lateinit var mListView: ListView + private lateinit var mProfile: VpnProfile + private lateinit var mDefaultAllowTextView: TextView + private lateinit var mListAdapter: PackageAdapter + private lateinit var mSettingsView: View + + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val avh = view.tag as AppViewHolder + avh.checkBox.toggle() + } + + override fun onClick(v: View) { + + } + + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + val packageName = buttonView.tag as String + if (isChecked) { + mProfile.mAllowedAppsVpn.add(packageName) + } else { + mProfile.mAllowedAppsVpn.remove(packageName) + } + } + + override fun onResume() { + super.onResume() + changeDisallowText(mProfile.mAllowedAppsVpnAreDisallowed) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val profileUuid = arguments.getString(activity.packageName + ".profileUUID") + mProfile = ProfileManager.get(activity, profileUuid) + activity.title = getString(R.string.edit_profile_title, mProfile.name) + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.allowed_apps, menu) + + val searchView = menu.findItem(R.id.app_search_widget).actionView as SearchView + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + mListView.setFilterText(query) + mListView.isTextFilterEnabled = true + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + mListView.setFilterText(newText) + mListView.isTextFilterEnabled = !TextUtils.isEmpty(newText) + + return true + } + }) + searchView.setOnCloseListener { + mListView.clearTextFilter() + mListAdapter.filter.filter("") + false + } + + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val v = inflater.inflate(R.layout.allowed_vpn_apps, container, false) + + mSettingsView = inflater.inflate(R.layout.allowed_application_settings, container, false) + mDefaultAllowTextView = mSettingsView.findViewById<View>(R.id.default_allow_text) as TextView + + val vpnOnDefaultSwitch = mSettingsView.findViewById<View>(R.id.default_allow) as Switch + + vpnOnDefaultSwitch.setOnCheckedChangeListener { buttonView, isChecked -> + changeDisallowText(isChecked) + mProfile.mAllowedAppsVpnAreDisallowed = isChecked + } + + vpnOnDefaultSwitch.isChecked = mProfile.mAllowedAppsVpnAreDisallowed + + val vpnAllowBypassSwitch = mSettingsView.findViewById<View>(R.id.allow_bypass) as Switch + + vpnAllowBypassSwitch.setOnCheckedChangeListener { buttonView, isChecked -> mProfile.mAllowAppVpnBypass = isChecked } + + vpnAllowBypassSwitch.isChecked = mProfile.mAllowAppVpnBypass + + mListView = v.findViewById<View>(android.R.id.list) as ListView + + mListAdapter = PackageAdapter(activity, mProfile) + mListView.adapter = mListAdapter + mListView.onItemClickListener = this + + mListView.emptyView = v.findViewById(R.id.loading_container) + + Thread(Runnable { mListAdapter.populateList(activity) }).start() + + return v + } + + private fun changeDisallowText(selectedAreDisallowed: Boolean) { + if (selectedAreDisallowed) + mDefaultAllowTextView.setText(R.string.vpn_disallow_radio) + else + mDefaultAllowTextView.setText(R.string.vpn_allow_radio) + } + + internal class AppViewHolder { + var mInfo: ApplicationInfo? = null + var rootView: View? = null + lateinit var appName: TextView + lateinit var appIcon: ImageView + //public TextView appSize; + //public TextView disabled; + lateinit var checkBox: CompoundButton + + companion object { + + fun createOrRecycle(inflater: LayoutInflater, oldview: View?, parent: ViewGroup): AppViewHolder { + var convertView = oldview + if (convertView == null) { + convertView = inflater.inflate(R.layout.allowed_application_layout, parent, false) + + // Creates a ViewHolder and store references to the two children views + // we want to bind data to. + val holder = AppViewHolder() + holder.rootView = convertView + holder.appName = convertView.findViewById<View>(R.id.app_name) as TextView + holder.appIcon = convertView.findViewById<View>(R.id.app_icon) as ImageView + //holder.appSize = (TextView) convertView.findViewById(R.id.app_size); + //holder.disabled = (TextView) convertView.findViewById(R.id.app_disabled); + holder.checkBox = convertView.findViewById<View>(R.id.app_selected) as CompoundButton + convertView.tag = holder + + + return holder + } else { + // Get the ViewHolder back to get fast access to the TextView + // and the ImageView. + return convertView.tag as AppViewHolder + } + } + } + + } + + internal inner class PackageAdapter(c: Context, vp: VpnProfile) : BaseAdapter(), Filterable { + private val mInflater: LayoutInflater = LayoutInflater.from(c) + private val mPm: PackageManager = c.packageManager + private var mPackages: Vector<ApplicationInfo> = Vector() + private val mFilter = ItemFilter() + private var mFilteredData: Vector<ApplicationInfo> = mPackages + private val mProfile = vp + + + fun populateList(c: Activity) { + val installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA) + + // Remove apps not using Internet + + var androidSystemUid = 0 + val apps = Vector<ApplicationInfo>() + + try { + val system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA) + androidSystemUid = system.uid + apps.add(system) + } catch (e: PackageManager.NameNotFoundException) { + } + + + for (app in installedPackages) { + + if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED && app.uid != androidSystemUid) { + + apps.add(app) + } + } + + Collections.sort(apps, ApplicationInfo.DisplayNameComparator(mPm)) + mPackages = apps + mFilteredData = apps + c.runOnUiThread { notifyDataSetChanged() } + } + + override fun getCount(): Int { + return mFilteredData.size + 1 + } + + override fun getItem(position: Int): Any { + return mFilteredData[position - 1] + } + + override fun getItemId(position: Int): Long { + if (position == 0) + return "settings".hashCode().toLong() + return mFilteredData[position - 1].packageName.hashCode().toLong() + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? { + if (position == 0) { + return mSettingsView + } else + return getViewApp(position - 1, convertView, parent) + + } + + fun getViewApp(position: Int, convertView: View?, parent: ViewGroup): View? { + val viewHolder = AppViewHolder.createOrRecycle(mInflater, convertView, parent) + viewHolder.mInfo = mFilteredData[position] + val mInfo = mFilteredData[position] + + + var appName = mInfo.loadLabel(mPm) + + if (TextUtils.isEmpty(appName)) + appName = mInfo.packageName + viewHolder.appName.text = appName + viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm)) + viewHolder.checkBox.tag = mInfo.packageName + viewHolder.checkBox.setOnCheckedChangeListener(this@Settings_Allowed_Apps) + + + viewHolder.checkBox.isChecked = mProfile.mAllowedAppsVpn.contains(mInfo.packageName) + return viewHolder.rootView + } + + override fun getFilter(): Filter { + return mFilter + } + + override fun getViewTypeCount(): Int { + return 2; + } + + override fun getItemViewType(position: Int): Int { + return if (position == 0) 0 else 1 + } + + private inner class ItemFilter : Filter() { + override fun performFiltering(constraint: CharSequence): Filter.FilterResults { + + val filterString = constraint.toString().toLowerCase(Locale.getDefault()) + + val results = Filter.FilterResults() + + + val count = mPackages.size + val nlist = Vector<ApplicationInfo>(count) + + for (i in 0 until count) { + val pInfo = mPackages[i] + var appName = pInfo.loadLabel(mPm) + + if (TextUtils.isEmpty(appName)) + appName = pInfo.packageName + + if (appName is String) { + if (appName.toLowerCase(Locale.getDefault()).contains(filterString)) + nlist.add(pInfo) + } else { + if (appName.toString().toLowerCase(Locale.getDefault()).contains(filterString)) + nlist.add(pInfo) + } + } + results.values = nlist + results.count = nlist.size + + return results + } + + override fun publishResults(constraint: CharSequence, results: Filter.FilterResults) { + mFilteredData = results.values as Vector<ApplicationInfo> + notifyDataSetChanged() + } + + } + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java new file mode 100644 index 00000000..8fd6aa98 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.SwitchPreference; +import android.text.TextUtils; +import android.util.Pair; +import de.blinkt.openvpn.activities.FileSelect; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.core.VpnStatus; +import de.blinkt.openvpn.views.RemoteCNPreference; +import de.blinkt.openvpn.VpnProfile; + +import java.io.IOException; + + +public class Settings_Authentication extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener, OnPreferenceClickListener { + private static final int SELECT_TLS_FILE_LEGACY_DIALOG = 23223232; + private static final int SELECT_TLS_FILE_KITKAT = SELECT_TLS_FILE_LEGACY_DIALOG +1; + private CheckBoxPreference mExpectTLSCert; + private CheckBoxPreference mCheckRemoteCN; + private RemoteCNPreference mRemoteCN; + private ListPreference mTLSAuthDirection; + private Preference mTLSAuthFile; + private SwitchPreference mUseTLSAuth; + private EditTextPreference mCipher; + private String mTlsAuthFileData; + private EditTextPreference mAuth; + private EditTextPreference mRemoteX509Name; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.vpn_authentification); + + mExpectTLSCert = (CheckBoxPreference) findPreference("remoteServerTLS"); + mCheckRemoteCN = (CheckBoxPreference) findPreference("checkRemoteCN"); + mRemoteCN = (RemoteCNPreference) findPreference("remotecn"); + mRemoteCN.setOnPreferenceChangeListener(this); + + mRemoteX509Name = (EditTextPreference) findPreference("remotex509name"); + mRemoteX509Name.setOnPreferenceChangeListener(this); + + mUseTLSAuth = (SwitchPreference) findPreference("useTLSAuth" ); + mTLSAuthFile = findPreference("tlsAuthFile"); + mTLSAuthDirection = (ListPreference) findPreference("tls_direction"); + + + mTLSAuthFile.setOnPreferenceClickListener(this); + + mCipher =(EditTextPreference) findPreference("cipher"); + mCipher.setOnPreferenceChangeListener(this); + + mAuth =(EditTextPreference) findPreference("auth"); + mAuth.setOnPreferenceChangeListener(this); + + loadSettings(); + + } + + @Override + protected void loadSettings() { + + mExpectTLSCert.setChecked(mProfile.mExpectTLSCert); + mCheckRemoteCN.setChecked(mProfile.mCheckRemoteCN); + mRemoteCN.setDN(mProfile.mRemoteCN); + mRemoteCN.setAuthType(mProfile.mX509AuthType); + onPreferenceChange(mRemoteCN, + new Pair<Integer, String>(mProfile.mX509AuthType, mProfile.mRemoteCN)); + + mRemoteX509Name.setText(mProfile.mx509UsernameField); + onPreferenceChange(mRemoteX509Name, mProfile.mx509UsernameField); + + mUseTLSAuth.setChecked(mProfile.mUseTLSAuth); + mTlsAuthFileData= mProfile.mTLSAuthFilename; + setTlsAuthSummary(mTlsAuthFileData); + mTLSAuthDirection.setValue(mProfile.mTLSAuthDirection); + mCipher.setText(mProfile.mCipher); + onPreferenceChange(mCipher, mProfile.mCipher); + mAuth.setText(mProfile.mAuth); + onPreferenceChange(mAuth, mProfile.mAuth); + + if (mProfile.mAuthenticationType == VpnProfile.TYPE_STATICKEYS) { + mExpectTLSCert.setEnabled(false); + mCheckRemoteCN.setEnabled(false); + mUseTLSAuth.setChecked(true); + } else { + mExpectTLSCert.setEnabled(true); + mCheckRemoteCN.setEnabled(true); + + } + } + + @Override + protected void saveSettings() { + mProfile.mExpectTLSCert=mExpectTLSCert.isChecked(); + mProfile.mCheckRemoteCN=mCheckRemoteCN.isChecked(); + mProfile.mRemoteCN=mRemoteCN.getCNText(); + mProfile.mX509AuthType=mRemoteCN.getAuthtype(); + + mProfile.mUseTLSAuth = mUseTLSAuth.isChecked(); + mProfile.mTLSAuthFilename = mTlsAuthFileData; + mProfile.mx509UsernameField = mRemoteX509Name.getText(); + + if(mTLSAuthDirection.getValue()==null) + mProfile.mTLSAuthDirection=null; + else + mProfile.mTLSAuthDirection = mTLSAuthDirection.getValue(); + + if(mCipher.getText()==null) + mProfile.mCipher=null; + else + mProfile.mCipher = mCipher.getText(); + + if(mAuth.getText()==null) + mProfile.mAuth = null; + else + mProfile.mAuth = mAuth.getText(); + + } + + + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if(preference==mRemoteCN) { + @SuppressWarnings("unchecked") + int authtype = ((Pair<Integer, String>) newValue).first; + @SuppressWarnings("unchecked") + String dn = ((Pair<Integer, String>) newValue).second; + + if ("".equals(dn)) { + if (mProfile.mConnections.length > 0) { + preference.setSummary(getX509String(VpnProfile.X509_VERIFY_TLSREMOTE_RDN, mProfile.mConnections[0].mServerName)); + } else { + preference.setSummary(R.string.no_remote_defined); + } + } else { + preference.setSummary(getX509String(authtype, dn)); + } + + } else if (preference == mCipher || preference == mAuth) { + preference.setSummary((CharSequence) newValue); + } else if (preference == mRemoteX509Name) { + preference.setSummary(TextUtils.isEmpty((CharSequence) newValue) ? "CN (default)" : (CharSequence)newValue); + } + return true; + } + private CharSequence getX509String(int authtype, String dn) { + String ret =""; + switch (authtype) { + case VpnProfile.X509_VERIFY_TLSREMOTE: + case VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING: + ret+="tls-remote "; + break; + + case VpnProfile.X509_VERIFY_TLSREMOTE_DN: + ret="dn: "; + break; + + case VpnProfile.X509_VERIFY_TLSREMOTE_RDN: + ret="rdn: "; + break; + + case VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX: + ret="rdn prefix: "; + break; + } + return ret + dn; + } + + void startFileDialog() { + Intent startFC = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && ! Utils.alwaysUseOldFileChooser(getActivity())) { + startFC = Utils.getFilePickerIntent(getActivity(), Utils.FileType.TLS_AUTH_FILE); + startActivityForResult(startFC, SELECT_TLS_FILE_KITKAT); + } + + if (startFC == null) { + startFC = new Intent(getActivity(), FileSelect.class); + startFC.putExtra(FileSelect.START_DATA, mTlsAuthFileData); + startFC.putExtra(FileSelect.WINDOW_TITLE, R.string.tls_auth_file); + startActivityForResult(startFC, SELECT_TLS_FILE_LEGACY_DIALOG); + } + } + + @Override + public boolean onPreferenceClick(Preference preference) { + startFileDialog(); + return true; + + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if(requestCode == SELECT_TLS_FILE_LEGACY_DIALOG && resultCode == Activity.RESULT_OK){ + String result = data.getStringExtra(FileSelect.RESULT_DATA); + mTlsAuthFileData=result; + setTlsAuthSummary(result); + } else if (requestCode == SELECT_TLS_FILE_KITKAT && resultCode == Activity.RESULT_OK) { + try { + mTlsAuthFileData= Utils.getFilePickerResult(Utils.FileType.TLS_AUTH_FILE,data,getActivity()); + setTlsAuthSummary(mTlsAuthFileData); + } catch (IOException e) { + VpnStatus.logException(e); + } catch (SecurityException se) { + VpnStatus.logException(se); + } + } + } + + private void setTlsAuthSummary(String result) { + if(result==null) + result = getString(R.string.no_certificate); + if(result.startsWith(VpnProfile.INLINE_TAG)) + mTLSAuthFile.setSummary(R.string.inline_file_data); + else if (result.startsWith(VpnProfile.DISPLAYNAME_TAG)) + mTLSAuthFile.setSummary(getString(R.string.imported_from_file, VpnProfile.getDisplayName(result))); + else + mTLSAuthFile.setSummary(result); + } +}
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java new file mode 100644 index 00000000..81da76fe --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.*; +import android.widget.AdapterView.OnItemSelectedListener; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.R.id; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ProfileManager; +import de.blinkt.openvpn.views.FileSelectLayout; + +public class Settings_Basic extends KeyChainSettingsFragment implements OnItemSelectedListener, FileSelectLayout.FileSelectCallback { + private static final int CHOOSE_FILE_OFFSET = 1000; + + private FileSelectLayout mClientCert; + private FileSelectLayout mCaCert; + private FileSelectLayout mClientKey; + private CheckBox mUseLzo; + private Spinner mType; + private FileSelectLayout mpkcs12; + private FileSelectLayout mCrlFile; + private TextView mPKCS12Password; + private EditText mUserName; + private EditText mPassword; + private View mView; + private EditText mProfileName; + private EditText mKeyPassword; + + private SparseArray<FileSelectLayout> fileselects = new SparseArray<>(); + private Spinner mAuthRetry; + + + private void addFileSelectLayout(FileSelectLayout fsl, Utils.FileType type) { + int i = fileselects.size() + CHOOSE_FILE_OFFSET; + fileselects.put(i, fsl); + fsl.setCaller(this, i, type); + } + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + + mView = inflater.inflate(R.layout.basic_settings, container, false); + + mProfileName = mView.findViewById(id.profilename); + mClientCert = mView.findViewById(id.certselect); + mClientKey = mView.findViewById(id.keyselect); + mCaCert = mView.findViewById(id.caselect); + mpkcs12 = mView.findViewById(id.pkcs12select); + mCrlFile = mView.findViewById(id.crlfile); + mUseLzo = mView.findViewById(id.lzo); + mType = mView.findViewById(id.type); + mPKCS12Password = mView.findViewById(id.pkcs12password); + + mUserName = mView.findViewById(id.auth_username); + mPassword = mView.findViewById(id.auth_password); + mKeyPassword = mView.findViewById(id.key_password); + mAuthRetry = mView.findViewById(id.auth_retry); + + addFileSelectLayout(mCaCert, Utils.FileType.CA_CERTIFICATE); + addFileSelectLayout(mClientCert, Utils.FileType.CLIENT_CERTIFICATE); + addFileSelectLayout(mClientKey, Utils.FileType.KEYFILE); + addFileSelectLayout(mpkcs12, Utils.FileType.PKCS12); + addFileSelectLayout(mCrlFile, Utils.FileType.CRL_FILE); + mCaCert.setShowClear(); + mCrlFile.setShowClear(); + + mType.setOnItemSelectedListener(this); + mAuthRetry.setOnItemSelectedListener(this); + + initKeychainViews(mView); + + return mView; + } + + + @Override + public void onActivityResult(int request, int result, Intent data) { + super.onActivityResult(request, result, data); + if (result == Activity.RESULT_OK && request >= CHOOSE_FILE_OFFSET) { + FileSelectLayout fsl = fileselects.get(request); + fsl.parseResponse(data, getActivity()); + + savePreferences(); + + // Private key files may result in showing/hiding the private key password dialog + if (fsl == mClientKey) { + changeType(mType.getSelectedItemPosition()); + } + } + + } + + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + if (parent == mType) { + changeType(position); + } + } + + + private void changeType(int type) { + // hide everything + mView.findViewById(R.id.pkcs12).setVisibility(View.GONE); + mView.findViewById(R.id.certs).setVisibility(View.GONE); + mView.findViewById(R.id.statickeys).setVisibility(View.GONE); + mView.findViewById(R.id.keystore).setVisibility(View.GONE); + mView.findViewById(R.id.cacert).setVisibility(View.GONE); + ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(false); + mView.findViewById(R.id.userpassword).setVisibility(View.GONE); + mView.findViewById(R.id.key_password_layout).setVisibility(View.GONE); + mView.findViewById(R.id.external_auth).setVisibility(View.GONE); + + // Fall through are by design + switch (type) { + case VpnProfile.TYPE_USERPASS_CERTIFICATES: + mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE); + case VpnProfile.TYPE_CERTIFICATES: + mView.findViewById(R.id.certs).setVisibility(View.VISIBLE); + mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE); + if (mProfile.requireTLSKeyPassword()) + mView.findViewById(R.id.key_password_layout).setVisibility(View.VISIBLE); + break; + + case VpnProfile.TYPE_USERPASS_PKCS12: + mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE); + case VpnProfile.TYPE_PKCS12: + mView.findViewById(R.id.pkcs12).setVisibility(View.VISIBLE); + mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE); + ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(true); + break; + + case VpnProfile.TYPE_STATICKEYS: + mView.findViewById(R.id.statickeys).setVisibility(View.VISIBLE); + break; + + case VpnProfile.TYPE_USERPASS_KEYSTORE: + mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE); + case VpnProfile.TYPE_KEYSTORE: + mView.findViewById(R.id.keystore).setVisibility(View.VISIBLE); + mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE); + ((FileSelectLayout) mView.findViewById(R.id.caselect)).setClearable(true); + break; + + case VpnProfile.TYPE_USERPASS: + mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE); + mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE); + break; + case VpnProfile.TYPE_EXTERNAL_APP: + mView.findViewById(R.id.external_auth).setVisibility(View.VISIBLE); + break; + } + + + } + + protected void loadPreferences() { + super.loadPreferences(); + mProfileName.setText(mProfile.mName); + mClientCert.setData(mProfile.mClientCertFilename, getActivity()); + mClientKey.setData(mProfile.mClientKeyFilename, getActivity()); + mCaCert.setData(mProfile.mCaFilename, getActivity()); + mCrlFile.setData(mProfile.mCrlFilename, getActivity()); + + mUseLzo.setChecked(mProfile.mUseLzo); + mType.setSelection(mProfile.mAuthenticationType); + mpkcs12.setData(mProfile.mPKCS12Filename, getActivity()); + mPKCS12Password.setText(mProfile.mPKCS12Password); + mUserName.setText(mProfile.mUsername); + mPassword.setText(mProfile.mPassword); + mKeyPassword.setText(mProfile.mKeyPassword); + mAuthRetry.setSelection(mProfile.mAuthRetry); + } + + protected void savePreferences() { + super.savePreferences(); + mProfile.mName = mProfileName.getText().toString(); + mProfile.mCaFilename = mCaCert.getData(); + mProfile.mClientCertFilename = mClientCert.getData(); + mProfile.mClientKeyFilename = mClientKey.getData(); + mProfile.mCrlFilename = mCrlFile.getData(); + + mProfile.mUseLzo = mUseLzo.isChecked(); + mProfile.mAuthenticationType = mType.getSelectedItemPosition(); + mProfile.mPKCS12Filename = mpkcs12.getData(); + mProfile.mPKCS12Password = mPKCS12Password.getText().toString(); + + mProfile.mPassword = mPassword.getText().toString(); + mProfile.mUsername = mUserName.getText().toString(); + mProfile.mKeyPassword = mKeyPassword.getText().toString(); + mProfile.mAuthRetry = mAuthRetry.getSelectedItemPosition(); + + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + savePreferences(); + if (mProfile != null) { + outState.putString(getActivity().getPackageName() + "profileUUID", mProfile.getUUID().toString()); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java new file mode 100644 index 00000000..e41e6cb9 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Checkable; +import android.widget.ImageButton; +import android.widget.TextView; + +import de.blinkt.openvpn.R; + +public class Settings_Connections extends Settings_Fragment implements View.OnClickListener { + private ConnectionsAdapter mConnectionsAdapter; + private TextView mWarning; + private Checkable mUseRandomRemote; + private RecyclerView mRecyclerView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + inflater.inflate(R.menu.connections, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.connections, container, false); + + mWarning = (TextView) v.findViewById(R.id.noserver_active_warning); + mRecyclerView = (RecyclerView) v.findViewById(R.id.connection_recycler_view); + + int dpwidth = (int) (container.getWidth()/getResources().getDisplayMetrics().density); + int columns = dpwidth/290; + columns = Math.max(1, columns); + + mConnectionsAdapter = new ConnectionsAdapter(getActivity(), this, mProfile); + + //mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(columns, StaggeredGridLayoutManager.VERTICAL)); + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false)); + mRecyclerView.setAdapter(mConnectionsAdapter); + + ImageButton fab_button = (ImageButton) v.findViewById(R.id.add_new_remote); + if(fab_button!=null) + fab_button.setOnClickListener(this); + + mUseRandomRemote = (Checkable) v.findViewById(R.id.remote_random); + mUseRandomRemote.setChecked(mProfile.mRemoteRandom); + + + mConnectionsAdapter.displayWarningIfNoneEnabled(); + + return v; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.add_new_remote) { + mConnectionsAdapter.addRemote(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId()==R.id.add_new_remote) + mConnectionsAdapter.addRemote(); + return super.onOptionsItemSelected(item); + } + + @Override + protected void savePreferences() { + mConnectionsAdapter.saveProfile(); + mProfile.mRemoteRandom = mUseRandomRemote.isChecked(); + } + + public void setWarningVisible(int showWarning) { + mWarning.setVisibility(showWarning); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java new file mode 100644 index 00000000..738bd0e9 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012-2015 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Fragment; +import android.os.Bundle; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ProfileManager; + +public abstract class Settings_Fragment extends Fragment { + + protected VpnProfile mProfile; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String profileUuid = getArguments().getString(getActivity().getPackageName() + ".profileUUID"); + mProfile= ProfileManager.get(getActivity(), profileUuid); + getActivity().setTitle(getString(R.string.edit_profile_title, mProfile.getName())); + } + + + @Override + public void onPause() { + super.onPause(); + savePreferences(); + } + + protected abstract void savePreferences(); +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java new file mode 100644 index 00000000..daf407b8 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceManager; +import android.preference.SwitchPreference; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; + +public class Settings_IP extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener { + private EditTextPreference mIPv4; + private EditTextPreference mIPv6; + private SwitchPreference mUsePull; + private CheckBoxPreference mOverrideDNS; + private EditTextPreference mSearchdomain; + private EditTextPreference mDNS1; + private EditTextPreference mDNS2; + private CheckBoxPreference mNobind; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + + // Make sure default values are applied. In a real app, you would + // want this in a shared function that is used to retrieve the + // SharedPreferences wherever they are needed. + PreferenceManager.setDefaultValues(getActivity(), + R.xml.vpn_ipsettings, false); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.vpn_ipsettings); + mIPv4 = (EditTextPreference) findPreference("ipv4_address"); + mIPv6 = (EditTextPreference) findPreference("ipv6_address"); + mUsePull = (SwitchPreference) findPreference("usePull"); + mOverrideDNS = (CheckBoxPreference) findPreference("overrideDNS"); + mSearchdomain =(EditTextPreference) findPreference("searchdomain"); + mDNS1 = (EditTextPreference) findPreference("dns1"); + mDNS2 = (EditTextPreference) findPreference("dns2"); + mNobind = (CheckBoxPreference) findPreference("nobind"); + + mIPv4.setOnPreferenceChangeListener(this); + mIPv6.setOnPreferenceChangeListener(this); + mDNS1.setOnPreferenceChangeListener(this); + mDNS2.setOnPreferenceChangeListener(this); + mUsePull.setOnPreferenceChangeListener(this); + mOverrideDNS.setOnPreferenceChangeListener(this); + mSearchdomain.setOnPreferenceChangeListener(this); + + loadSettings(); + } + + @Override + protected void loadSettings() { + + mUsePull.setChecked(mProfile.mUsePull); + mIPv4.setText(mProfile.mIPv4Address); + mIPv6.setText(mProfile.mIPv6Address); + mDNS1.setText(mProfile.mDNS1); + mDNS2.setText(mProfile.mDNS2); + mOverrideDNS.setChecked(mProfile.mOverrideDNS); + mSearchdomain.setText(mProfile.mSearchDomain); + mNobind.setChecked(mProfile.mNobind); + if (mProfile.mAuthenticationType == VpnProfile.TYPE_STATICKEYS) + mUsePull.setChecked(false); + + mUsePull.setEnabled(mProfile.mAuthenticationType != VpnProfile.TYPE_STATICKEYS); + + // Sets Summary + onPreferenceChange(mIPv4, mIPv4.getText()); + onPreferenceChange(mIPv6, mIPv6.getText()); + onPreferenceChange(mDNS1, mDNS1.getText()); + onPreferenceChange(mDNS2, mDNS2.getText()); + onPreferenceChange(mSearchdomain, mSearchdomain.getText()); + + setDNSState(); + } + + + @Override + protected void saveSettings() { + mProfile.mUsePull = mUsePull.isChecked(); + mProfile.mIPv4Address = mIPv4.getText(); + mProfile.mIPv6Address = mIPv6.getText(); + mProfile.mDNS1 = mDNS1.getText(); + mProfile.mDNS2 = mDNS2.getText(); + mProfile.mOverrideDNS = mOverrideDNS.isChecked(); + mProfile.mSearchDomain = mSearchdomain.getText(); + mProfile.mNobind = mNobind.isChecked(); + + } + + @Override + public boolean onPreferenceChange(Preference preference, + Object newValue) { + if(preference==mIPv4 || preference == mIPv6 + || preference==mDNS1 || preference == mDNS2 + || preference == mSearchdomain + ) + + preference.setSummary((String)newValue); + + if(preference== mUsePull || preference == mOverrideDNS) + if(preference==mOverrideDNS) { + // Set so the function gets the right value + mOverrideDNS.setChecked((Boolean) newValue); + } + setDNSState(); + + saveSettings(); + return true; + } + + private void setDNSState() { + boolean enabled; + mOverrideDNS.setEnabled(mUsePull.isChecked()); + if(!mUsePull.isChecked()) + enabled =true; + else + enabled = mOverrideDNS.isChecked(); + + mDNS1.setEnabled(enabled); + mDNS2.setEnabled(enabled); + mSearchdomain.setEnabled(enabled); + + + } + + + }
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java new file mode 100644 index 00000000..6674599d --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.widget.Toast; + +import java.util.Locale; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; + +public class Settings_Obscure extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener { + private CheckBoxPreference mUseRandomHostName; + private CheckBoxPreference mUseFloat; + private CheckBoxPreference mUseCustomConfig; + private EditTextPreference mCustomConfig; + private EditTextPreference mMssFixValue; + private CheckBoxPreference mMssFixCheckBox; + private CheckBoxPreference mPeerInfo; + + private CheckBoxPreference mPersistent; + private ListPreference mConnectRetrymax; + private EditTextPreference mConnectRetry; + private EditTextPreference mConnectRetryMaxTime; + private EditTextPreference mTunMtu; + + public void onCreateBehaviour(Bundle savedInstanceState) { + + mPersistent = (CheckBoxPreference) findPreference("usePersistTun"); + mConnectRetrymax = (ListPreference) findPreference("connectretrymax"); + mConnectRetry = (EditTextPreference) findPreference("connectretry"); + mConnectRetryMaxTime = (EditTextPreference) findPreference("connectretrymaxtime"); + + mPeerInfo = (CheckBoxPreference) findPreference("peerInfo"); + + mConnectRetrymax.setOnPreferenceChangeListener(this); + mConnectRetrymax.setSummary("%s"); + + mConnectRetry.setOnPreferenceChangeListener(this); + mConnectRetryMaxTime.setOnPreferenceChangeListener(this); + + + + } + + protected void loadSettingsBehaviour() { + mPersistent.setChecked(mProfile.mPersistTun); + mPeerInfo.setChecked(mProfile.mPushPeerInfo); + + mConnectRetrymax.setValue(mProfile.mConnectRetryMax); + onPreferenceChange(mConnectRetrymax, mProfile.mConnectRetryMax); + + mConnectRetry.setText(mProfile.mConnectRetry); + onPreferenceChange(mConnectRetry, mProfile.mConnectRetry); + + mConnectRetryMaxTime.setText(mProfile.mConnectRetryMaxTime); + onPreferenceChange(mConnectRetryMaxTime, mProfile.mConnectRetryMaxTime); + + } + + + protected void saveSettingsBehaviour() { + mProfile.mConnectRetryMax = mConnectRetrymax.getValue(); + mProfile.mPersistTun = mPersistent.isChecked(); + mProfile.mConnectRetry = mConnectRetry.getText(); + mProfile.mPushPeerInfo = mPeerInfo.isChecked(); + mProfile.mConnectRetryMaxTime = mConnectRetryMaxTime.getText(); + } + + + public boolean onPreferenceChangeBehaviour(Preference preference, Object newValue) { + if (preference == mConnectRetrymax) { + if(newValue==null) { + newValue="5"; + } + mConnectRetrymax.setDefaultValue(newValue); + + for(int i=0;i< mConnectRetrymax.getEntryValues().length;i++){ + if(mConnectRetrymax.getEntryValues().equals(newValue)) + mConnectRetrymax.setSummary(mConnectRetrymax.getEntries()[i]); + } + + } else if (preference == mConnectRetry) { + if(newValue==null || newValue=="") + newValue="2"; + mConnectRetry.setSummary(String.format("%s s", newValue)); + } else if (preference == mConnectRetryMaxTime) { + if(newValue==null || newValue=="") + newValue="300"; + mConnectRetryMaxTime.setSummary(String.format("%s s", newValue)); + } + + + return true; + } + + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.vpn_obscure); + + mUseRandomHostName = (CheckBoxPreference) findPreference("useRandomHostname"); + mUseFloat = (CheckBoxPreference) findPreference("useFloat"); + mUseCustomConfig = (CheckBoxPreference) findPreference("enableCustomOptions"); + mCustomConfig = (EditTextPreference) findPreference("customOptions"); + mMssFixCheckBox = (CheckBoxPreference) findPreference("mssFix"); + mMssFixValue = (EditTextPreference) findPreference("mssFixValue"); + mMssFixValue.setOnPreferenceChangeListener(this); + mTunMtu = (EditTextPreference) findPreference("tunmtu"); + mTunMtu.setOnPreferenceChangeListener(this);; + + onCreateBehaviour(savedInstanceState); + loadSettings(); + + } + + protected void loadSettings() { + mUseRandomHostName.setChecked(mProfile.mUseRandomHostname); + mUseFloat.setChecked(mProfile.mUseFloat); + mUseCustomConfig.setChecked(mProfile.mUseCustomConfig); + mCustomConfig.setText(mProfile.mCustomConfigOptions); + + if (mProfile.mMssFix == 0) { + mMssFixValue.setText(String.valueOf(VpnProfile.DEFAULT_MSSFIX_SIZE)); + mMssFixCheckBox.setChecked(false); + setMssSummary(VpnProfile.DEFAULT_MSSFIX_SIZE); + } else { + mMssFixValue.setText(String.valueOf(mProfile.mMssFix)); + mMssFixCheckBox.setChecked(true); + setMssSummary(mProfile.mMssFix); + } + + + int tunmtu = mProfile.mTunMtu; + if (mProfile.mTunMtu < 48) + tunmtu = 1500; + + mTunMtu.setText(String.valueOf(tunmtu)); + setMtuSummary(tunmtu); + + + loadSettingsBehaviour(); + + } + + private void setMssSummary(int value) { + mMssFixValue.setSummary(String.format(Locale.getDefault(),"Configured MSS value: %d", value)); + } + + private void setMtuSummary(int value) { + if (value == 1500) + mTunMtu.setSummary(String.format(Locale.getDefault(),"Using default (1500) MTU", value)); + else + mTunMtu.setSummary(String.format(Locale.getDefault(),"Configured MTU value: %d", value)); + } + + protected void saveSettings() { + mProfile.mUseRandomHostname = mUseRandomHostName.isChecked(); + mProfile.mUseFloat = mUseFloat.isChecked(); + mProfile.mUseCustomConfig = mUseCustomConfig.isChecked(); + mProfile.mCustomConfigOptions = mCustomConfig.getText(); + if (mMssFixCheckBox.isChecked()) + mProfile.mMssFix=Integer.parseInt(mMssFixValue.getText()); + else + mProfile.mMssFix=0; + + mProfile.mTunMtu = Integer.parseInt(mTunMtu.getText()); + saveSettingsBehaviour(); + } + + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference.getKey().equals("mssFixValue")) + try { + int v = Integer.parseInt((String) newValue); + if (v < 0 || v > 9000) + throw new NumberFormatException("mssfix value"); + setMssSummary(v); + + } catch(NumberFormatException e) { + Toast.makeText(getActivity(), R.string.mssfix_invalid_value, Toast.LENGTH_LONG).show(); + return false; + } + else if (preference.getKey().equals("tunmtu")) + try { + int v = Integer.parseInt((String) newValue); + if (v < 48 || v > 9000) + throw new NumberFormatException("mtu value"); + setMtuSummary(v); + + } catch(NumberFormatException e) { + Toast.makeText(getActivity(), R.string.mtu_invalid_value, Toast.LENGTH_LONG).show(); + return false; + } + return onPreferenceChangeBehaviour(preference, newValue); + + } + +}
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java new file mode 100644 index 00000000..53f88bbf --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; +import android.os.Build; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import de.blinkt.openvpn.R; + + +public class Settings_Routing extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener { + private EditTextPreference mCustomRoutes; + private CheckBoxPreference mUseDefaultRoute; + private EditTextPreference mCustomRoutesv6; + private CheckBoxPreference mUseDefaultRoutev6; + private CheckBoxPreference mRouteNoPull; + private CheckBoxPreference mLocalVPNAccess; + private EditTextPreference mExcludedRoutes; + private EditTextPreference mExcludedRoutesv6; + private CheckBoxPreference mBlockUnusedAF; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.vpn_routing); + mCustomRoutes = (EditTextPreference) findPreference("customRoutes"); + mUseDefaultRoute = (CheckBoxPreference) findPreference("useDefaultRoute"); + mCustomRoutesv6 = (EditTextPreference) findPreference("customRoutesv6"); + mUseDefaultRoutev6 = (CheckBoxPreference) findPreference("useDefaultRoutev6"); + mExcludedRoutes = (EditTextPreference) findPreference("excludedRoutes"); + mExcludedRoutesv6 = (EditTextPreference) findPreference("excludedRoutesv6"); + + mRouteNoPull = (CheckBoxPreference) findPreference("routenopull"); + mLocalVPNAccess = (CheckBoxPreference) findPreference("unblockLocal"); + + mBlockUnusedAF = (CheckBoxPreference) findPreference("blockUnusedAF"); + + mCustomRoutes.setOnPreferenceChangeListener(this); + mCustomRoutesv6.setOnPreferenceChangeListener(this); + mExcludedRoutes.setOnPreferenceChangeListener(this); + mExcludedRoutesv6.setOnPreferenceChangeListener(this); + mBlockUnusedAF.setOnPreferenceChangeListener(this); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + getPreferenceScreen().removePreference(mBlockUnusedAF); + + loadSettings(); + } + + @Override + protected void loadSettings() { + + mUseDefaultRoute.setChecked(mProfile.mUseDefaultRoute); + mUseDefaultRoutev6.setChecked(mProfile.mUseDefaultRoutev6); + + mCustomRoutes.setText(mProfile.mCustomRoutes); + mCustomRoutesv6.setText(mProfile.mCustomRoutesv6); + + mExcludedRoutes.setText(mProfile.mExcludedRoutes); + mExcludedRoutesv6.setText(mProfile.mExcludedRoutesv6); + + mRouteNoPull.setChecked(mProfile.mRoutenopull); + mLocalVPNAccess.setChecked(mProfile.mAllowLocalLAN); + + mBlockUnusedAF.setChecked(mProfile.mBlockUnusedAddressFamilies); + + // Sets Summary + onPreferenceChange(mCustomRoutes, mCustomRoutes.getText()); + onPreferenceChange(mCustomRoutesv6, mCustomRoutesv6.getText()); + onPreferenceChange(mExcludedRoutes, mExcludedRoutes.getText()); + onPreferenceChange(mExcludedRoutesv6, mExcludedRoutesv6.getText()); + + mRouteNoPull.setEnabled(mProfile.mUsePull); + } + + + @Override + protected void saveSettings() { + mProfile.mUseDefaultRoute = mUseDefaultRoute.isChecked(); + mProfile.mUseDefaultRoutev6 = mUseDefaultRoutev6.isChecked(); + mProfile.mCustomRoutes = mCustomRoutes.getText(); + mProfile.mCustomRoutesv6 = mCustomRoutesv6.getText(); + mProfile.mRoutenopull = mRouteNoPull.isChecked(); + mProfile.mAllowLocalLAN =mLocalVPNAccess.isChecked(); + mProfile.mExcludedRoutes = mExcludedRoutes.getText(); + mProfile.mExcludedRoutesv6 = mExcludedRoutesv6.getText(); + mProfile.mBlockUnusedAddressFamilies = mBlockUnusedAF.isChecked(); + } + + @Override + public boolean onPreferenceChange(Preference preference, + Object newValue) { + if( preference == mCustomRoutes || preference == mCustomRoutesv6 + || preference == mExcludedRoutes || preference == mExcludedRoutesv6) + preference.setSummary((String)newValue); + + saveSettings(); + return true; + } + + +}
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java new file mode 100644 index 00000000..98ebb55b --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012-2015 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.api.AppRestrictions; + +public class Settings_UserEditable extends KeyChainSettingsFragment implements View.OnClickListener { + + private View mView; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mView = inflater.inflate(R.layout.settings_usereditable, container, false); + TextView messageView = (TextView) mView.findViewById(R.id.messageUserEdit); + messageView.setText(getString(R.string.message_no_user_edit, getPackageString(mProfile.mProfileCreator))); + initKeychainViews(this.mView); + return mView; + } + + + private String getPackageString(String packageName) { + + if (AppRestrictions.PROFILE_CREATOR.equals(packageName)) + return "Android Enterprise Management"; + + final PackageManager pm = getActivity().getPackageManager(); + ApplicationInfo ai; + try { + ai = pm.getApplicationInfo(packageName, 0); + } catch (final PackageManager.NameNotFoundException e) { + ai = null; + } + final String applicationName = (String) (ai != null ? pm.getApplicationLabel(ai) : "(unknown)"); + return String.format("%s (%s)", applicationName, packageName); + } + + @Override + protected void savePreferences() { + + } + + @Override + public void onResume() { + super.onResume(); + mView.findViewById(R.id.keystore).setVisibility(View.GONE); + if (mProfile.mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE || + mProfile.mAuthenticationType == VpnProfile.TYPE_KEYSTORE) + mView.findViewById(R.id.keystore).setVisibility(View.VISIBLE); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java b/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java new file mode 100644 index 00000000..f5c1750a --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Fragment; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ProfileManager; + + +public class ShowConfigFragment extends Fragment { + private String configtext; + private TextView mConfigView; + private ImageButton mfabButton; + + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + View v=inflater.inflate(R.layout.viewconfig, container,false); + mConfigView = (TextView) v.findViewById(R.id.configview); + + + mfabButton = (ImageButton) v.findViewById(R.id.share_config); + if (mfabButton!=null) { + mfabButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + shareConfig(); + } + }); + mfabButton.setVisibility(View.INVISIBLE); + } + return v; + } + + private void startGenConfig(final VpnProfile vp, final TextView cv) { + + new Thread() { + public void run() { + /* Add a few newlines to make the textview scrollable past the FAB */ + try { + + configtext = vp.getConfigFile(getActivity(), VpnProfile.doUseOpenVPN3(getActivity())) + "\n\n\n"; + } catch (Exception e) { + e.printStackTrace(); + configtext = "Error generating config file: " + e.getLocalizedMessage(); + } + getActivity().runOnUiThread(() -> { + cv.setText(configtext); + if (mfabButton!=null) + mfabButton.setVisibility(View.VISIBLE); + }); + + + } + }.start(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + inflater.inflate(R.menu.configmenu, menu); + } + + private void shareConfig() { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, configtext); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.export_config_title)); + shareIntent.setType("text/plain"); + startActivity(Intent.createChooser(shareIntent, getString(R.string.export_config_chooser_title))); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.sendConfig) { + shareConfig(); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onResume() { + super.onResume(); + + populateConfigText(); + } + + private void populateConfigText() { + String profileUUID = getArguments().getString(getActivity().getPackageName() + ".profileUUID"); + final VpnProfile vp = ProfileManager.get(getActivity(),profileUUID); + int check=vp.checkProfile(getActivity()); + + if(check != R.string.no_error_found) { + mConfigView.setText(check); + configtext = getString(check); + } + else { + // Run in own Thread since Keystore does not like to be queried from the main thread + + mConfigView.setText("Generating config..."); + startGenConfig(vp, mConfigView); + } + } + + @Override + public void setUserVisibleHint(boolean visible) + { + super.setUserVisibleHint(visible); + if (visible && isResumed()) + populateConfigText(); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java b/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java new file mode 100644 index 00000000..abdc45f5 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.OpenableColumns; +import android.util.Base64; +import android.webkit.MimeTypeMap; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.TreeSet; +import java.util.Vector; + +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.Preferences; + +public class Utils { + + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static Intent getFilePickerIntent(Context c, FileType fileType) { + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + TreeSet<String> supportedMimeTypes = new TreeSet<String>(); + Vector<String> extensions = new Vector<String>(); + + switch (fileType) { + case PKCS12: + i.setType("application/x-pkcs12"); + supportedMimeTypes.add("application/x-pkcs12"); + extensions.add("p12"); + extensions.add("pfx"); + break; + case CLIENT_CERTIFICATE: + case CA_CERTIFICATE: + i.setType("application/x-pem-file"); + supportedMimeTypes.add("application/x-x509-ca-cert"); + supportedMimeTypes.add("application/x-x509-user-cert"); + supportedMimeTypes.add("application/x-pem-file"); + supportedMimeTypes.add("application/pkix-cert"); + supportedMimeTypes.add("text/plain"); + + extensions.add("pem"); + extensions.add("crt"); + extensions.add("cer"); + break; + case KEYFILE: + i.setType("application/x-pem-file"); + supportedMimeTypes.add("application/x-pem-file"); + supportedMimeTypes.add("application/pkcs8"); + + // Google drive .... + supportedMimeTypes.add("application/x-iwork-keynote-sffkey"); + extensions.add("key"); + break; + + case TLS_AUTH_FILE: + i.setType("text/plain"); + + // Backup .... + supportedMimeTypes.add("application/pkcs8"); + // Google Drive is kind of crazy ..... + supportedMimeTypes.add("application/x-iwork-keynote-sffkey"); + + extensions.add("txt"); + extensions.add("key"); + break; + + case OVPN_CONFIG: + i.setType("application/x-openvpn-profile"); + supportedMimeTypes.add("application/x-openvpn-profile"); + supportedMimeTypes.add("application/openvpn-profile"); + supportedMimeTypes.add("application/ovpn"); + supportedMimeTypes.add("text/plain"); + extensions.add("ovpn"); + extensions.add("conf"); + break; + + case CRL_FILE: + supportedMimeTypes.add("application/x-pkcs7-crl"); + supportedMimeTypes.add("application/pkix-crl"); + extensions.add("crl"); + break; + + case USERPW_FILE: + i.setType("text/plain"); + supportedMimeTypes.add("text/plain"); + break; + } + + MimeTypeMap mtm = MimeTypeMap.getSingleton(); + + for (String ext : extensions) { + String mimeType = mtm.getMimeTypeFromExtension(ext); + if (mimeType != null) + supportedMimeTypes.add(mimeType); + } + + // Always add this as fallback + supportedMimeTypes.add("application/octet-stream"); + + i.putExtra(Intent.EXTRA_MIME_TYPES, supportedMimeTypes.toArray(new String[supportedMimeTypes.size()])); + + // People don't know that this is actually a system setting. Override it ... + // DocumentsContract.EXTRA_SHOW_ADVANCED is hidden + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + + /* Samsung has decided to do something strange, on stock Android GET_CONTENT opens the document UI */ + /* fist try with documentsui */ + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) + i.setPackage("com.android.documentsui"); + + + + + //noinspection ConstantConditions + if (!isIntentAvailable(c,i)) { + i.setAction(Intent.ACTION_OPEN_DOCUMENT); + i.setPackage(null); + + // Check for really broken devices ... :( + if (!isIntentAvailable(c,i)) { + return null; + } + } + + + /* + final PackageManager packageManager = c.getPackageManager(); + ResolveInfo list = packageManager.resolveActivity(i, 0); + + Toast.makeText(c, "Starting package: "+ list.activityInfo.packageName + + "with ACTION " + i.getAction(), Toast.LENGTH_LONG).show(); + + */ + return i; + } + + public static boolean alwaysUseOldFileChooser(Context c) + { + /* Android P does not allow access to the file storage anymore */ + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) + return false; + + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(c); + return prefs.getBoolean("useInternalFileSelector", false); + } + + public static boolean isIntentAvailable(Context context, Intent i) { + final PackageManager packageManager = context.getPackageManager(); + List<ResolveInfo> list = + packageManager.queryIntentActivities(i, + PackageManager.MATCH_DEFAULT_ONLY); + + // Ignore the Android TV framework app in the list + int size = list.size(); + for (ResolveInfo ri: list) + { + // Ignore stub apps + if ("com.google.android.tv.frameworkpackagestubs".equals(ri.activityInfo.packageName)) + { + size--; + } + } + + return size > 0; + } + + + public enum FileType { + PKCS12(0), + CLIENT_CERTIFICATE(1), + CA_CERTIFICATE(2), + OVPN_CONFIG(3), + KEYFILE(4), + TLS_AUTH_FILE(5), + USERPW_FILE(6), + CRL_FILE(7); + + private int value; + + FileType(int i) { + value = i; + } + + public static FileType getFileTypeByValue(int value) { + switch (value) { + case 0: + return PKCS12; + case 1: + return CLIENT_CERTIFICATE; + case 2: + return CA_CERTIFICATE; + case 3: + return OVPN_CONFIG; + case 4: + return KEYFILE; + case 5: + return TLS_AUTH_FILE; + case 6: + return USERPW_FILE; + case 7: + return CRL_FILE; + default: + return null; + } + } + + public int getValue() { + return value; + } + } + + static private byte[] readBytesFromStream(InputStream input) throws IOException { + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + int nRead; + byte[] data = new byte[16384]; + + ; + + long totalread = 0; + while ((nRead = input.read(data, 0, data.length)) != -1 && totalread <VpnProfile.MAX_EMBED_FILE_SIZE ) { + buffer.write(data, 0, nRead); + totalread+=nRead; + } + + buffer.flush(); + input.close(); + return buffer.toByteArray(); + } + + public static String getFilePickerResult(FileType ft, Intent result, Context c) throws IOException, SecurityException { + + Uri uri = result.getData(); + if (uri == null) + return null; + + byte[] fileData = readBytesFromStream(c.getContentResolver().openInputStream(uri)); + String newData = null; + + Cursor cursor = c.getContentResolver().query(uri, null, null, null, null); + + String prefix = ""; + try { + if (cursor!=null && cursor.moveToFirst()) { + int cidx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (cidx != -1) { + String displayName = cursor.getString(cidx); + + if (!displayName.contains(VpnProfile.INLINE_TAG) && !displayName.contains(VpnProfile.DISPLAYNAME_TAG)) + prefix = VpnProfile.DISPLAYNAME_TAG + displayName; + } + } + } finally { + if(cursor!=null) + cursor.close(); + } + + switch (ft) { + case PKCS12: + newData = Base64.encodeToString(fileData, Base64.DEFAULT); + break; + default: + newData = new String(fileData, "UTF-8"); + break; + } + + return prefix + VpnProfile.INLINE_TAG + newData; + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java new file mode 100644 index 00000000..7ad13aaf --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java @@ -0,0 +1,636 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ListFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.support.annotation.RequiresApi; +import android.text.Html; +import android.text.Html.ImageGetter; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.TreeSet; + +import de.blinkt.openvpn.LaunchVPN; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.activities.ConfigConverter; +import de.blinkt.openvpn.activities.DisconnectVPN; +import de.blinkt.openvpn.activities.FileSelect; +import de.blinkt.openvpn.activities.VPNPreferences; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.Preferences; +import de.blinkt.openvpn.core.ProfileManager; +import de.blinkt.openvpn.core.VpnStatus; + +import static de.blinkt.openvpn.core.OpenVPNService.DISCONNECT_VPN; + + +public class VPNProfileList extends ListFragment implements OnClickListener, VpnStatus.StateListener { + + public final static int RESULT_VPN_DELETED = Activity.RESULT_FIRST_USER; + public final static int RESULT_VPN_DUPLICATE = Activity.RESULT_FIRST_USER + 1; + + private static final int MENU_ADD_PROFILE = Menu.FIRST; + + private static final int START_VPN_CONFIG = 92; + private static final int SELECT_PROFILE = 43; + private static final int IMPORT_PROFILE = 231; + private static final int FILE_PICKER_RESULT_KITKAT = 392; + + private static final int MENU_IMPORT_PROFILE = Menu.FIRST + 1; + private static final int MENU_CHANGE_SORTING = Menu.FIRST + 2; + private static final String PREF_SORT_BY_LRU = "sortProfilesByLRU"; + private String mLastStatusMessage; + + @Override + public void updateState(String state, String logmessage, final int localizedResId, ConnectionStatus level) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mLastStatusMessage = VpnStatus.getLastCleanLogMessage(getActivity()); + mArrayadapter.notifyDataSetChanged(); + } + }); + } + + @Override + public void setConnectedVPN(String uuid) { + } + + private class VPNArrayAdapter extends ArrayAdapter<VpnProfile> { + + public VPNArrayAdapter(Context context, int resource, + int textViewResourceId) { + super(context, resource, textViewResourceId); + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + View v = super.getView(position, convertView, parent); + + final VpnProfile profile = (VpnProfile) getListAdapter().getItem(position); + + View titleview = v.findViewById(R.id.vpn_list_item_left); + titleview.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + startOrStopVPN(profile); + } + }); + + View settingsview = v.findViewById(R.id.quickedit_settings); + settingsview.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + editVPN(profile); + + } + }); + + TextView subtitle = (TextView) v.findViewById(R.id.vpn_item_subtitle); + if (profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) { + subtitle.setText(mLastStatusMessage); + subtitle.setVisibility(View.VISIBLE); + } else { + subtitle.setText(""); + subtitle.setVisibility(View.GONE); + } + + + return v; + } + } + + private void startOrStopVPN(VpnProfile profile) { + if (VpnStatus.isVPNActive() && profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) { + Intent disconnectVPN = new Intent(getActivity(), DisconnectVPN.class); + startActivity(disconnectVPN); + } else { + startVPN(profile); + } + } + + + private ArrayAdapter<VpnProfile> mArrayadapter; + + protected VpnProfile mEditProfile = null; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + + // Shortcut version is increased to refresh all shortcuts + final static int SHORTCUT_VERSION = 1; + + @RequiresApi(api = Build.VERSION_CODES.N_MR1) + void updateDynamicShortcuts() { + PersistableBundle versionExtras = new PersistableBundle(); + versionExtras.putInt("version", SHORTCUT_VERSION); + + ShortcutManager shortcutManager = getContext().getSystemService(ShortcutManager.class); + if (shortcutManager.isRateLimitingActive()) + return; + + List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts(); + int maxvpn = shortcutManager.getMaxShortcutCountPerActivity() - 1; + + + ShortcutInfo disconnectShortcut = new ShortcutInfo.Builder(getContext(), "disconnectVPN") + .setShortLabel("Disconnect") + .setLongLabel("Disconnect VPN") + .setIntent(new Intent(getContext(), DisconnectVPN.class).setAction(DISCONNECT_VPN)) + .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_cancel)) + .setExtras(versionExtras) + .build(); + + LinkedList<ShortcutInfo> newShortcuts = new LinkedList<>(); + LinkedList<ShortcutInfo> updateShortcuts = new LinkedList<>(); + + LinkedList<String> removeShortcuts = new LinkedList<>(); + LinkedList<String> disableShortcuts = new LinkedList<>(); + + boolean addDisconnect = true; + + + TreeSet<VpnProfile> sortedProfilesLRU = new TreeSet<VpnProfile>(new VpnProfileLRUComparator()); + ProfileManager profileManager = ProfileManager.getInstance(getContext()); + sortedProfilesLRU.addAll(profileManager.getProfiles()); + + LinkedList<VpnProfile> LRUProfiles = new LinkedList<>(); + maxvpn = Math.min(maxvpn, sortedProfilesLRU.size()); + + for (int i = 0; i < maxvpn; i++) { + LRUProfiles.add(sortedProfilesLRU.pollFirst()); + } + + for (ShortcutInfo shortcut : shortcuts) { + if (shortcut.getId().equals("disconnectVPN")) { + addDisconnect = false; + if (shortcut.getExtras() == null + || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION) + updateShortcuts.add(disconnectShortcut); + + } else { + VpnProfile p = ProfileManager.get(getContext(), shortcut.getId()); + if (p == null || p.profileDeleted) { + if (shortcut.isEnabled()) { + disableShortcuts.add(shortcut.getId()); + removeShortcuts.add(shortcut.getId()); + } + if (!shortcut.isPinned()) + removeShortcuts.add(shortcut.getId()); + } else { + + if (LRUProfiles.contains(p)) + LRUProfiles.remove(p); + else + removeShortcuts.add(p.getUUIDString()); + + if (!p.getName().equals(shortcut.getShortLabel()) + || shortcut.getExtras() == null + || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION) + updateShortcuts.add(createShortcut(p)); + + + } + + } + + } + if (addDisconnect) + newShortcuts.add(disconnectShortcut); + for (VpnProfile p : LRUProfiles) + newShortcuts.add(createShortcut(p)); + + if (updateShortcuts.size() > 0) + shortcutManager.updateShortcuts(updateShortcuts); + if (removeShortcuts.size() > 0) + shortcutManager.removeDynamicShortcuts(removeShortcuts); + if (newShortcuts.size() > 0) + shortcutManager.addDynamicShortcuts(newShortcuts); + if (disableShortcuts.size() > 0) + shortcutManager.disableShortcuts(disableShortcuts, "VpnProfile does not exist anymore."); + } + + @RequiresApi(Build.VERSION_CODES.N_MR1) + ShortcutInfo createShortcut(VpnProfile profile) { + Intent shortcutIntent = new Intent(Intent.ACTION_MAIN); + shortcutIntent.setClass(getActivity(), LaunchVPN.class); + shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString()); + shortcutIntent.setAction(Intent.ACTION_MAIN); + shortcutIntent.putExtra("EXTRA_HIDELOG", true); + + PersistableBundle versionExtras = new PersistableBundle(); + versionExtras.putInt("version", SHORTCUT_VERSION); + + return new ShortcutInfo.Builder(getContext(), profile.getUUIDString()) + .setShortLabel(profile.getName()) + .setLongLabel(getString(R.string.qs_connect, profile.getName())) + .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_vpn_key)) + .setIntent(shortcutIntent) + .setExtras(versionExtras) + .build(); + } + + class MiniImageGetter implements ImageGetter { + + + @Override + public Drawable getDrawable(String source) { + Drawable d = null; + if ("ic_menu_add".equals(source)) + d = getActivity().getResources().getDrawable(R.drawable.ic_menu_add_grey); + else if ("ic_menu_archive".equals(source)) + d = getActivity().getResources().getDrawable(R.drawable.ic_menu_import_grey); + + + if (d != null) { + d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); + return d; + } else { + return null; + } + } + } + + + @Override + public void onResume() { + super.onResume(); + setListAdapter(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + updateDynamicShortcuts(); + } + VpnStatus.addStateListener(this); + } + + @Override + public void onPause() { + super.onPause(); + VpnStatus.removeStateListener(this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.vpn_profile_list, container, false); + + TextView newvpntext = (TextView) v.findViewById(R.id.add_new_vpn_hint); + TextView importvpntext = (TextView) v.findViewById(R.id.import_vpn_hint); + + newvpntext.setText(Html.fromHtml(getString(R.string.add_new_vpn_hint), new MiniImageGetter(), null)); + importvpntext.setText(Html.fromHtml(getString(R.string.vpn_import_hint), new MiniImageGetter(), null)); + + ImageButton fab_add = (ImageButton) v.findViewById(R.id.fab_add); + ImageButton fab_import = (ImageButton) v.findViewById(R.id.fab_import); + if (fab_add != null) + fab_add.setOnClickListener(this); + + if (fab_import != null) + fab_import.setOnClickListener(this); + + return v; + + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setListAdapter(); + } + + static class VpnProfileNameComparator implements Comparator<VpnProfile> { + + @Override + public int compare(VpnProfile lhs, VpnProfile rhs) { + if (lhs == rhs) + // Catches also both null + return 0; + + if (lhs == null) + return -1; + if (rhs == null) + return 1; + + if (lhs.mName == null) + return -1; + if (rhs.mName == null) + return 1; + + return lhs.mName.compareTo(rhs.mName); + } + + } + + static class VpnProfileLRUComparator implements Comparator<VpnProfile> { + + VpnProfileNameComparator nameComparator = new VpnProfileNameComparator(); + + @Override + public int compare(VpnProfile lhs, VpnProfile rhs) { + if (lhs == rhs) + // Catches also both null + return 0; + + if (lhs == null) + return -1; + if (rhs == null) + return 1; + + // Copied from Long.compare + if (lhs.mLastUsed > rhs.mLastUsed) + return -1; + if (lhs.mLastUsed < rhs.mLastUsed) + return 1; + else + return nameComparator.compare(lhs, rhs); + } + } + + + private void setListAdapter() { + if (mArrayadapter == null) { + mArrayadapter = new VPNArrayAdapter(getActivity(), R.layout.vpn_list_item, R.id.vpn_item_title); + + } + populateVpnList(); + } + + private void populateVpnList() { + boolean sortByLRU = Preferences.getDefaultSharedPreferences(getActivity()).getBoolean(PREF_SORT_BY_LRU, false); + Collection<VpnProfile> allvpn = getPM().getProfiles(); + TreeSet<VpnProfile> sortedset; + if (sortByLRU) + sortedset = new TreeSet<>(new VpnProfileLRUComparator()); + else + sortedset = new TreeSet<>(new VpnProfileNameComparator()); + + sortedset.addAll(allvpn); + mArrayadapter.clear(); + mArrayadapter.addAll(sortedset); + + setListAdapter(mArrayadapter); + mArrayadapter.notifyDataSetChanged(); + } + + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile) + .setIcon(R.drawable.ic_menu_add) + .setAlphabeticShortcut('a') + .setTitleCondensed(getActivity().getString(R.string.add)) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + + menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import) + .setIcon(R.drawable.ic_menu_import) + .setAlphabeticShortcut('i') + .setTitleCondensed(getActivity().getString(R.string.menu_import_short)) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + + menu.add(0, MENU_CHANGE_SORTING, 0, R.string.change_sorting) + .setIcon(R.drawable.ic_sort) + .setAlphabeticShortcut('s') + .setTitleCondensed(getString(R.string.sort)) + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM); + + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == MENU_ADD_PROFILE) { + onAddOrDuplicateProfile(null); + return true; + } else if (itemId == MENU_IMPORT_PROFILE) { + return startImportConfigFilePicker(); + } else if (itemId == MENU_CHANGE_SORTING) { + return changeSorting(); + } else { + return super.onOptionsItemSelected(item); + } + } + + private boolean changeSorting() { + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(getActivity()); + boolean oldValue = prefs.getBoolean(PREF_SORT_BY_LRU, false); + SharedPreferences.Editor prefsedit = prefs.edit(); + if (oldValue) { + Toast.makeText(getActivity(), R.string.sorted_az, Toast.LENGTH_SHORT).show(); + prefsedit.putBoolean(PREF_SORT_BY_LRU, false); + } else { + prefsedit.putBoolean(PREF_SORT_BY_LRU, true); + Toast.makeText(getActivity(), R.string.sorted_lru, Toast.LENGTH_SHORT).show(); + } + prefsedit.apply(); + populateVpnList(); + return true; + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.fab_import: + startImportConfigFilePicker(); + break; + case R.id.fab_add: + onAddOrDuplicateProfile(null); + break; + } + } + + private boolean startImportConfigFilePicker() { + boolean startOldFileDialog = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !Utils.alwaysUseOldFileChooser(getActivity() )) + startOldFileDialog = !startFilePicker(); + + if (startOldFileDialog) + startImportConfig(); + + return true; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private boolean startFilePicker() { + + Intent i = Utils.getFilePickerIntent(getActivity(), Utils.FileType.OVPN_CONFIG); + if (i != null) { + startActivityForResult(i, FILE_PICKER_RESULT_KITKAT); + return true; + } else + return false; + } + + private void startImportConfig() { + Intent intent = new Intent(getActivity(), FileSelect.class); + intent.putExtra(FileSelect.NO_INLINE_SELECTION, true); + intent.putExtra(FileSelect.WINDOW_TITLE, R.string.import_configuration_file); + startActivityForResult(intent, SELECT_PROFILE); + } + + + private void onAddOrDuplicateProfile(final VpnProfile mCopyProfile) { + Context context = getActivity(); + if (context != null) { + final EditText entry = new EditText(context); + entry.setSingleLine(); + + AlertDialog.Builder dialog = new AlertDialog.Builder(context); + if (mCopyProfile == null) + dialog.setTitle(R.string.menu_add_profile); + else { + dialog.setTitle(context.getString(R.string.duplicate_profile_title, mCopyProfile.mName)); + entry.setText(getString(R.string.copy_of_profile, mCopyProfile.mName)); + } + + dialog.setMessage(R.string.add_profile_name_prompt); + dialog.setView(entry); + + dialog.setNeutralButton(R.string.menu_import_short, + (dialog1, which) -> startImportConfigFilePicker()); + dialog.setPositiveButton(android.R.string.ok, + (dialog12, which) -> { + String name = entry.getText().toString(); + if (getPM().getProfileByName(name) == null) { + VpnProfile profile; + if (mCopyProfile != null) { + profile = mCopyProfile.copy(name); + // Remove restrictions on copy profile + profile.mProfileCreator = null; + profile.mUserEditable = true; + } else + profile = new VpnProfile(name); + + addProfile(profile); + editVPN(profile); + } else { + Toast.makeText(getActivity(), R.string.duplicate_profile_name, Toast.LENGTH_LONG).show(); + } + }); + dialog.setNegativeButton(android.R.string.cancel, null); + dialog.create().show(); + } + + } + + private void addProfile(VpnProfile profile) { + getPM().addProfile(profile); + getPM().saveProfileList(getActivity()); + getPM().saveProfile(getActivity(), profile); + mArrayadapter.add(profile); + } + + private ProfileManager getPM() { + return ProfileManager.getInstance(getActivity()); + } + + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (resultCode == RESULT_VPN_DELETED) { + if (mArrayadapter != null && mEditProfile != null) + mArrayadapter.remove(mEditProfile); + } else if (resultCode == RESULT_VPN_DUPLICATE && data != null) { + String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID); + VpnProfile profile = ProfileManager.get(getActivity(), profileUUID); + if (profile != null) + onAddOrDuplicateProfile(profile); + } + + + if (resultCode != Activity.RESULT_OK) + return; + + + if (requestCode == START_VPN_CONFIG) { + String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID); + + VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN); + getPM().saveProfile(getActivity(), profile); + // Name could be modified, reset List adapter + setListAdapter(); + + } else if (requestCode == SELECT_PROFILE) { + String fileData = data.getStringExtra(FileSelect.RESULT_DATA); + Uri uri = new Uri.Builder().path(fileData).scheme("file").build(); + + startConfigImport(uri); + } else if (requestCode == IMPORT_PROFILE) { + String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID); + mArrayadapter.add(ProfileManager.get(getActivity(), profileUUID)); + } else if (requestCode == FILE_PICKER_RESULT_KITKAT) { + if (data != null) { + Uri uri = data.getData(); + startConfigImport(uri); + } + } + + } + + private void startConfigImport(Uri uri) { + Intent startImport = new Intent(getActivity(), ConfigConverter.class); + startImport.setAction(ConfigConverter.IMPORT_PROFILE); + startImport.setData(uri); + startActivityForResult(startImport, IMPORT_PROFILE); + } + + + private void editVPN(VpnProfile profile) { + mEditProfile = profile; + Intent vprefintent = new Intent(getActivity(), VPNPreferences.class) + .putExtra(getActivity().getPackageName() + ".profileUUID", profile.getUUID().toString()); + + startActivityForResult(vprefintent, START_VPN_CONFIG); + } + + private void startVPN(VpnProfile profile) { + + getPM().saveProfile(getActivity(), profile); + + Intent intent = new Intent(getActivity(), LaunchVPN.class); + intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString()); + intent.setAction(Intent.ACTION_MAIN); + startActivity(intent); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java b/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java new file mode 100644 index 00000000..e8328f5c --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ProfileManager; + +import java.util.Collection; + +public class DefaultVPNListPreference extends ListPreference { + public DefaultVPNListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setVPNs(context); + } + + private void setVPNs(Context c) { + ProfileManager pm = ProfileManager.getInstance(c); + Collection<VpnProfile> profiles = pm.getProfiles(); + CharSequence[] entries = new CharSequence[profiles.size()]; + CharSequence[] entryValues = new CharSequence[profiles.size()];; + + int i=0; + for (VpnProfile p: profiles) + { + entries[i]=p.getName(); + entryValues[i]=p.getUUIDString(); + i++; + } + + setEntries(entries); + setEntryValues(entryValues); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java b/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java new file mode 100644 index 00000000..bc3bd5cd --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.IOException; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.activities.FileSelect; +import de.blinkt.openvpn.core.VpnStatus; +import de.blinkt.openvpn.core.X509Utils; +import de.blinkt.openvpn.fragments.Utils; + +import static android.os.Build.VERSION; +import static android.os.Build.VERSION_CODES; + + +public class FileSelectLayout extends LinearLayout implements OnClickListener { + + + public void parseResponse(Intent data, Context c) { + + try { + String newData = Utils.getFilePickerResult(fileType, data, c); + if (newData!=null) + setData(newData, c); + + if (newData == null) { + String fileData = data.getStringExtra(FileSelect.RESULT_DATA); + setData(fileData, c); + } + + } catch (IOException | SecurityException e) { + VpnStatus.logException(e); + } + } + + public interface FileSelectCallback { + + String getString(int res); + + void startActivityForResult(Intent startFC, int mTaskId); + } + + private boolean mIsCertificate; + private TextView mDataView; + private String mData; + private FileSelectCallback mFragment; + private int mTaskId; + private Button mSelectButton; + private Utils.FileType fileType; + private String mTitle; + private boolean mShowClear; + private TextView mDataDetails; + private Button mShowClearButton; + + + public FileSelectLayout(Context context, AttributeSet attrset) { + super(context, attrset); + + TypedArray ta = context.obtainStyledAttributes(attrset, R.styleable.FileSelectLayout); + + setupViews(ta.getString(R.styleable.FileSelectLayout_fileTitle), ta.getBoolean(R.styleable.FileSelectLayout_certificate, true) + ); + + ta.recycle(); + } + + public FileSelectLayout (Context context, String title, boolean isCertificate, boolean showClear) + { + super(context); + + setupViews(title, isCertificate); + mShowClear = showClear; + + } + + private void setupViews(String title, boolean isCertificate) { + inflate(getContext(), R.layout.file_select, this); + + mTitle = title; + mIsCertificate = isCertificate; + + TextView tView = (TextView) findViewById(R.id.file_title); + tView.setText(mTitle); + + mDataView = (TextView) findViewById(R.id.file_selected_item); + mDataDetails = (TextView) findViewById(R.id.file_selected_description); + mSelectButton = (Button) findViewById(R.id.file_select_button); + mSelectButton.setOnClickListener(this); + + mShowClearButton = (Button) findViewById(R.id.file_clear_button); + mShowClearButton.setOnClickListener(this); + } + + public void setClearable(boolean clearable) + { + mShowClear = clearable; + if (mShowClearButton != null && mData !=null) + mShowClearButton.setVisibility(mShowClear? VISIBLE : GONE); + + } + + + public void setCaller(FileSelectCallback fragment, int i, Utils.FileType ft) { + mTaskId = i; + mFragment = fragment; + fileType = ft; + } + + public void getCertificateFileDialog() { + Intent startFC = new Intent(getContext(), FileSelect.class); + startFC.putExtra(FileSelect.START_DATA, mData); + startFC.putExtra(FileSelect.WINDOW_TITLE, mTitle); + if (fileType == Utils.FileType.PKCS12) + startFC.putExtra(FileSelect.DO_BASE64_ENCODE, true); + if (mShowClear) + startFC.putExtra(FileSelect.SHOW_CLEAR_BUTTON, true); + + mFragment.startActivityForResult(startFC, mTaskId); + } + + + public String getData() { + return mData; + } + + public void setData(String data, Context c) { + mData = data; + if (data == null) { + mDataView.setText(c.getString(R.string.no_data)); + mDataDetails.setText(""); + mShowClearButton.setVisibility(GONE); + } else { + if (mData.startsWith(VpnProfile.DISPLAYNAME_TAG)) { + mDataView.setText(c.getString(R.string.imported_from_file, VpnProfile.getDisplayName(mData))); + } else if (mData.startsWith(VpnProfile.INLINE_TAG)) + mDataView.setText(R.string.inline_file_data); + else + mDataView.setText(data); + if (mIsCertificate) { + mDataDetails.setText(X509Utils.getCertificateFriendlyName(c, data)); + } + + // Show clear button if it should be shown + mShowClearButton.setVisibility(mShowClear? VISIBLE : GONE); + } + + } + + @Override + public void onClick(View v) { + if (v == mSelectButton) { + Intent startFilePicker=null; + if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + startFilePicker = Utils.getFilePickerIntent(getContext(), fileType); + } + + if (startFilePicker == null || Utils.alwaysUseOldFileChooser(v.getContext())) { + getCertificateFileDialog(); + } else { + mFragment.startActivityForResult(startFilePicker, mTaskId); + } + } else if (v == mShowClearButton) { + setData(null, getContext()); + } + } + + + + + public void setShowClear() { + mShowClear = true; + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java b/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java new file mode 100644 index 00000000..8296a644 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RadioGroup; + +import java.util.HashMap; +import java.util.Map; + +public class MultiLineRadioGroup extends RadioGroup { + private Map<View, Rect> viewRectMap; + + public MultiLineRadioGroup(Context context) { + this(context, null); + } + + public MultiLineRadioGroup(Context context, AttributeSet attrs) { + super(context, attrs); + + viewRectMap = new HashMap<View, Rect>(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + int widthMeasurement = MeasureSpec.getSize(widthMeasureSpec); + int heightMeasurement = MeasureSpec.getSize(heightMeasureSpec); + switch (getOrientation()){ + case HORIZONTAL: + heightMeasurement = findHorizontalHeight(widthMeasureSpec, heightMeasureSpec); + break; + case VERTICAL: + widthMeasurement = findVerticalWidth(widthMeasureSpec, heightMeasureSpec); + break; + } + setMeasuredDimension(widthMeasurement, heightMeasurement); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int count = getChildCount(); + for(int x=0; x < count; ++x) { + View button = getChildAt(x); + Rect dims = viewRectMap.get(button); + button.layout(dims.left, dims.top, dims.right, dims.bottom); + } + } + + private int findHorizontalHeight(int widthMeasureSpec, int heightMeasureSpec){ + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + int maxRight = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight(); + + // create MeasureSpecs to accommodate max space that RadioButtons can occupy + int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxRight - getPaddingLeft(), + MeasureSpec.getMode(widthMeasureSpec)); + int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + parentHeight - (getPaddingTop() + getPaddingBottom()), + MeasureSpec.getMode(heightMeasureSpec)); + + int nextLeft = getPaddingLeft(); + int nextTop = getPaddingTop(); + int maxRowHeight = 0; + viewRectMap.clear(); + // measure and find placement for each RadioButton (results to be used in onLayout() stage) + int count = getChildCount(); + for(int x=0; x < count; ++x){ + View button = getChildAt(x); + measureChild(button, newWidthMeasureSpec, newHeightMeasureSpec); + + maxRowHeight = Math.max(maxRowHeight, button.getMeasuredHeight()); + + // determine RadioButton placement + int nextRight = nextLeft + button.getMeasuredWidth(); + if(nextRight > maxRight){ // if current button will exceed border on this row ... + // ... move to next row + nextLeft = getPaddingLeft(); + nextTop += maxRowHeight; + + // adjust for next row values + nextRight = nextLeft + button.getMeasuredWidth(); + maxRowHeight = button.getMeasuredHeight(); + } + + int nextBottom = nextTop + button.getMeasuredHeight(); + viewRectMap.put(button, new Rect(nextLeft, nextTop, nextRight, nextBottom)); + + // update nextLeft + nextLeft = nextRight; + } + + // height of RadioGroup is a natural by-product of placing all the children + int idealHeight = nextTop + maxRowHeight + getPaddingBottom(); + switch(MeasureSpec.getMode(heightMeasureSpec)){ + case MeasureSpec.UNSPECIFIED: + return idealHeight; + case MeasureSpec.AT_MOST: + return Math.min(idealHeight, parentHeight); + case MeasureSpec.EXACTLY: + default: + return parentHeight; + } + } + + private int findVerticalWidth(int widthMeasureSpec, int heightMeasureSpec){ + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int maxBottom = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom(); + + // create MeasureSpecs to accommodate max space that RadioButtons can occupy + int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + parentWidth - (getPaddingLeft() + getPaddingRight()), + MeasureSpec.getMode(widthMeasureSpec)); + int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxBottom - getPaddingTop(), + MeasureSpec.getMode(heightMeasureSpec)); + + int nextTop = getPaddingTop(); + int nextLeft = getPaddingLeft(); + int maxColWidth = 0; + viewRectMap.clear(); + // measure and find placement for each RadioButton (results to be used in onLayout() stage) + int count = getChildCount(); + for(int x=0; x < count; ++x){ + View button = getChildAt(x); + measureChild(button, newWidthMeasureSpec, newHeightMeasureSpec); + + maxColWidth = Math.max(maxColWidth, button.getMeasuredWidth()); + + // determine RadioButton placement + int nextBottom = nextTop + button.getMeasuredHeight(); + if(nextBottom > maxBottom){ // if current button will exceed border for this column ... + // ... move to next column + nextTop = getPaddingTop(); + nextLeft += maxColWidth; + + // adjust for next row values + nextBottom = nextTop + button.getMeasuredHeight(); + maxColWidth = button.getMeasuredWidth(); + } + + int nextRight = nextLeft + button.getMeasuredWidth(); + viewRectMap.put(button, new Rect(nextLeft, nextTop, nextRight, nextBottom)); + + // update nextTop + nextTop = nextBottom; + } + + // width of RadioGroup is a natural by-product of placing all the children + int idealWidth = nextLeft + maxColWidth + getPaddingRight(); + switch(MeasureSpec.getMode(widthMeasureSpec)){ + case MeasureSpec.UNSPECIFIED: + return idealWidth; + case MeasureSpec.AT_MOST: + return Math.min(idealWidth, parentWidth); + case MeasureSpec.EXACTLY: + default: + return parentWidth; + } + } +}
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java b/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java new file mode 100644 index 00000000..ab8598c6 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java @@ -0,0 +1,732 @@ +/* + * Copyright (C) 2013 Andreas Stuetz <andreas.stuetz@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.blinkt.openvpn.views; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.ViewCompat; +import android.support.v4n.view.ViewPager; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Pair; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + + +import java.util.Locale; + +import de.blinkt.openvpn.R; + +public class PagerSlidingTabStrip extends HorizontalScrollView implements TabBarView { + + private static final float OPAQUE = 1.0f; + private static final float HALF_TRANSP = 0.5f; + + public interface CustomTabProvider { + public View getCustomTabView(ViewGroup parent, int position); + } + + // @formatter:off + private static final int[] ATTRS = new int[]{ + android.R.attr.textSize, + android.R.attr.textColor, + android.R.attr.paddingLeft, + android.R.attr.paddingRight, + }; + // @formatter:on + + private final PagerAdapterObserver adapterObserver = new PagerAdapterObserver(); + + //These indexes must be related with the ATTR array above + private static final int TEXT_SIZE_INDEX = 0; + private static final int TEXT_COLOR_INDEX = 1; + private static final int PADDING_LEFT_INDEX = 2; + private static final int PADDING_RIGHT_INDEX = 3; + + private LinearLayout.LayoutParams defaultTabLayoutParams; + private LinearLayout.LayoutParams expandedTabLayoutParams; + + private final PageListener pageListener = new PageListener(); + public ViewPager.OnPageChangeListener delegatePageListener; + + private LinearLayout tabsContainer; + private ViewPager pager; + + private int tabCount; + + private int currentPosition = 0; + private float currentPositionOffset = 0f; + + private Paint rectPaint; + private Paint dividerPaint; + + private int indicatorColor; + private int indicatorHeight = 2; + + private int underlineHeight = 0; + private int underlineColor; + + private int dividerWidth = 0; + private int dividerPadding = 0; + private int dividerColor; + + private int tabPadding = 12; + private int tabTextSize = 14; + private ColorStateList tabTextColor = null; + private float tabTextAlpha = HALF_TRANSP; + private float tabTextSelectedAlpha = OPAQUE; + + private int paddingLeft = 0; + private int paddingRight = 0; + + private boolean shouldExpand = false; + private boolean textAllCaps = true; + private boolean isPaddingMiddle = false; + + private Typeface tabTypeface = null; + private int tabTypefaceStyle = Typeface.BOLD; + private int tabTypefaceSelectedStyle = Typeface.BOLD; + + private int scrollOffset; + private int lastScrollX = 0; + + private int tabBackgroundResId = R.drawable.slidingtab_background; + + private Locale locale; + + public PagerSlidingTabStrip(Context context) { + this(context, null); + } + + public PagerSlidingTabStrip(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagerSlidingTabStrip(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setFillViewport(true); + setWillNotDraw(false); + tabsContainer = new LinearLayout(context); + tabsContainer.setOrientation(LinearLayout.HORIZONTAL); + tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + addView(tabsContainer); + + //Default color will be 'textColorPrimary' + int colorPrimary = context.getResources().getColor(android.R.color.primary_text_dark); + setTextColor(colorPrimary); + underlineColor = colorPrimary; + dividerColor = colorPrimary; + indicatorColor = colorPrimary; + + + DisplayMetrics dm = getResources().getDisplayMetrics(); + scrollOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, scrollOffset, dm); + indicatorHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, indicatorHeight, dm); + underlineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, underlineHeight, dm); + dividerPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerPadding, dm); + tabPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, tabPadding, dm); + dividerWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerWidth, dm); + tabTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, tabTextSize, dm); + + // get system attrs (android:textSize and android:textColor) + TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + tabTextSize = a.getDimensionPixelSize(TEXT_SIZE_INDEX, tabTextSize); + ColorStateList colorStateList = a.getColorStateList(TEXT_COLOR_INDEX); + if (colorStateList != null) { + tabTextColor = colorStateList; + } + paddingLeft = a.getDimensionPixelSize(PADDING_LEFT_INDEX, paddingLeft); + paddingRight = a.getDimensionPixelSize(PADDING_RIGHT_INDEX, paddingRight); + a.recycle(); + + //In case we have the padding they must be equal so we take the biggest + if (paddingRight < paddingLeft) { + paddingRight = paddingLeft; + } + + if (paddingLeft < paddingRight) { + paddingLeft = paddingRight; + } + + // get custom attrs + a = context.obtainStyledAttributes(attrs, R.styleable.PagerSlidingTabStrip); + indicatorColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsIndicatorColor, indicatorColor); + underlineColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsUnderlineColor, underlineColor); + dividerColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsDividerColor, dividerColor); + dividerWidth = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerWidth, dividerWidth); + indicatorHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsIndicatorHeight, indicatorHeight); + underlineHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsUnderlineHeight, underlineHeight); + dividerPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerPadding, dividerPadding); + tabPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsTabPaddingLeftRight, tabPadding); + tabBackgroundResId = a.getResourceId(R.styleable.PagerSlidingTabStrip_pstsTabBackground, tabBackgroundResId); + shouldExpand = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsShouldExpand, shouldExpand); + scrollOffset = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsScrollOffset, scrollOffset); + textAllCaps = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsTextAllCaps, textAllCaps); + isPaddingMiddle = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsPaddingMiddle, isPaddingMiddle); + tabTypefaceStyle = a.getInt(R.styleable.PagerSlidingTabStrip_pstsTextStyle, Typeface.BOLD); + tabTypefaceSelectedStyle = a.getInt(R.styleable.PagerSlidingTabStrip_pstsTextSelectedStyle, Typeface.BOLD); + tabTextAlpha = a.getFloat(R.styleable.PagerSlidingTabStrip_pstsTextAlpha, HALF_TRANSP); + tabTextSelectedAlpha = a.getFloat(R.styleable.PagerSlidingTabStrip_pstsTextSelectedAlpha, OPAQUE); + a.recycle(); + + rectPaint = new Paint(); + rectPaint.setAntiAlias(true); + rectPaint.setStyle(Style.FILL); + + + dividerPaint = new Paint(); + dividerPaint.setAntiAlias(true); + dividerPaint.setStrokeWidth(dividerWidth); + + defaultTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + expandedTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f); + + if (locale == null) { + locale = getResources().getConfiguration().locale; + } + } + + public void setViewPager(ViewPager pager) { + this.pager = pager; + if (pager.getAdapter() == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + + pager.setOnPageChangeListener(pageListener); + pager.getAdapter().registerDataSetObserver(adapterObserver); + adapterObserver.setAttached(true); + notifyDataSetChanged(); + } + + public void notifyDataSetChanged() { + tabsContainer.removeAllViews(); + tabCount = pager.getAdapter().getCount(); + View tabView; + for (int i = 0; i < tabCount; i++) { + + if (pager.getAdapter() instanceof CustomTabProvider) { + tabView = ((CustomTabProvider) pager.getAdapter()).getCustomTabView(this, i); + } else { + tabView = LayoutInflater.from(getContext()).inflate(R.layout.padersliding_tab, this, false); + } + + CharSequence title = pager.getAdapter().getPageTitle(i); + + addTab(i, title, tabView); + } + + updateTabStyles(); + getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + @Override + public void onGlobalLayout() { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + + currentPosition = pager.getCurrentItem(); + currentPositionOffset = 0f; + scrollToChild(currentPosition, 0); + updateSelection(currentPosition); + } + }); + } + + private void addTab(final int position, CharSequence title, View tabView) { + TextView textView = (TextView) tabView.findViewById(R.id.tab_title); + if (textView != null) { + if (title != null) textView.setText(title); + float alpha = pager.getCurrentItem() == position ? tabTextSelectedAlpha : tabTextAlpha; + ViewCompat.setAlpha(textView, alpha); + } + + tabView.setFocusable(true); + tabView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (pager.getCurrentItem() != position) { + View tab = tabsContainer.getChildAt(pager.getCurrentItem()); + notSelected(tab); + pager.setCurrentItem(position); + } + } + }); + + tabView.setPadding(tabPadding, tabView.getPaddingTop(), tabPadding, tabView.getPaddingBottom()); + tabsContainer.addView(tabView, position, shouldExpand ? expandedTabLayoutParams : defaultTabLayoutParams); + } + + private void updateTabStyles() { + for (int i = 0; i < tabCount; i++) { + View v = tabsContainer.getChildAt(i); + v.setBackgroundResource(tabBackgroundResId); + TextView tab_title = (TextView) v.findViewById(R.id.tab_title); + + if (tab_title != null) { + tab_title.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabTextSize); + tab_title.setTypeface(tabTypeface, pager.getCurrentItem() == i ? tabTypefaceSelectedStyle : tabTypefaceStyle); + if (tabTextColor != null) { + tab_title.setTextColor(tabTextColor); + } + // setAllCaps() is only available from API 14, so the upper case is made manually if we are on a + // pre-ICS-build + if (textAllCaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + tab_title.setAllCaps(true); + } else { + tab_title.setText(tab_title.getText().toString().toUpperCase(locale)); + } + } + } + } + + } + + private void scrollToChild(int position, int offset) { + if (tabCount == 0) { + return; + } + + int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset; + if (position > 0 || offset > 0) { + + //Half screen offset. + //- Either tabs start at the middle of the view scrolling straight away + //- Or tabs start at the begging (no padding) scrolling when indicator gets + // to the middle of the view width + newScrollX -= scrollOffset; + Pair<Float, Float> lines = getIndicatorCoordinates(); + newScrollX += ((lines.second - lines.first) / 2); + } + + if (newScrollX != lastScrollX) { + lastScrollX = newScrollX; + scrollTo(newScrollX, 0); + } + } + + private Pair<Float, Float> getIndicatorCoordinates() { + // default: line below current tab + View currentTab = tabsContainer.getChildAt(currentPosition); + float lineLeft = currentTab.getLeft(); + float lineRight = currentTab.getRight(); + + // if there is an offset, start interpolating left and right coordinates between current and next tab + if (currentPositionOffset > 0f && currentPosition < tabCount - 1) { + + View nextTab = tabsContainer.getChildAt(currentPosition + 1); + final float nextTabLeft = nextTab.getLeft(); + final float nextTabRight = nextTab.getRight(); + + lineLeft = (currentPositionOffset * nextTabLeft + (1f - currentPositionOffset) * lineLeft); + lineRight = (currentPositionOffset * nextTabRight + (1f - currentPositionOffset) * lineRight); + } + return new Pair<Float, Float>(lineLeft, lineRight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (isInEditMode() || tabCount == 0) { + return; + } + + final int height = getHeight(); + // draw indicator line + rectPaint.setColor(indicatorColor); + Pair<Float, Float> lines = getIndicatorCoordinates(); + canvas.drawRect(lines.first + paddingLeft, height - indicatorHeight, lines.second + paddingRight, height, rectPaint); + // draw underline + rectPaint.setColor(underlineColor); + canvas.drawRect(paddingLeft, height - underlineHeight, tabsContainer.getWidth() + paddingRight, height, rectPaint); + // draw divider + if (dividerWidth != 0) { + dividerPaint.setStrokeWidth(dividerWidth); + dividerPaint.setColor(dividerColor); + for (int i = 0; i < tabCount - 1; i++) { + View tab = tabsContainer.getChildAt(i); + canvas.drawLine(tab.getRight(), dividerPadding, tab.getRight(), height - dividerPadding, dividerPaint); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (isPaddingMiddle) { + //Make sure tabContainer is bigger than the HorizontalScrollView to be able to scroll + tabsContainer.setMinimumWidth(getWidth()); + int halfFirstTab = 0; + if (tabsContainer.getChildCount() > 0) { + halfFirstTab = (tabsContainer.getChildAt(0).getWidth() / 2); + } + //The user choose the tabs to start in the middle of the view width (padding) + paddingLeft = paddingRight = getWidth() / 2 - halfFirstTab; + //Clipping padding to false to see the tabs while we pass them swiping + setClipToPadding(false); + } + + if (scrollOffset == 0) scrollOffset = getWidth() / 2 - paddingLeft; + setPadding(paddingLeft, getPaddingTop(), paddingRight, getPaddingBottom()); + super.onLayout(changed, l, t, r, b); + } + + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + this.delegatePageListener = listener; + } + + private class PageListener implements ViewPager.OnPageChangeListener { + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + currentPosition = position; + currentPositionOffset = positionOffset; + int offset = tabCount > 0 ? (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()) : 0; + scrollToChild(position, offset); + invalidate(); + if (delegatePageListener != null) { + delegatePageListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + if (state == ViewPager.SCROLL_STATE_IDLE) { + scrollToChild(pager.getCurrentItem(), 0); + } + //Full alpha for current item + View currentTab = tabsContainer.getChildAt(pager.getCurrentItem()); + selected(currentTab); + //Half transparent for prev item + if (pager.getCurrentItem() - 1 >= 0) { + View prevTab = tabsContainer.getChildAt(pager.getCurrentItem() - 1); + notSelected(prevTab); + } + //Half transparent for next item + if (pager.getCurrentItem() + 1 <= pager.getAdapter().getCount() - 1) { + View nextTab = tabsContainer.getChildAt(pager.getCurrentItem() + 1); + notSelected(nextTab); + } + + if (delegatePageListener != null) { + delegatePageListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + updateSelection(position); + if (delegatePageListener != null) { + delegatePageListener.onPageSelected(position); + } + } + + } + + private void updateSelection(int position) { + for (int i = 0; i < tabCount; ++i) { + View tv = tabsContainer.getChildAt(i); + tv.setSelected(i == position); + } + } + + private void notSelected(View tab) { + TextView title = (TextView) tab.findViewById(R.id.tab_title); + if (title != null) { + title.setTypeface(tabTypeface, tabTypefaceStyle); + ViewCompat.setAlpha(title, tabTextAlpha); + } + } + + private void selected(View tab) { + TextView title = (TextView) tab.findViewById(R.id.tab_title); + if (title != null) { + title.setTypeface(tabTypeface, tabTypefaceSelectedStyle); + ViewCompat.setAlpha(title, tabTextSelectedAlpha); + } + } + + private class PagerAdapterObserver extends DataSetObserver { + + private boolean attached = false; + + @Override + public void onChanged() { + notifyDataSetChanged(); + } + + public void setAttached(boolean attached) { + this.attached = attached; + } + + public boolean isAttached() { + return attached; + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (pager != null) { + if (!adapterObserver.isAttached()) { + pager.getAdapter().registerDataSetObserver(adapterObserver); + adapterObserver.setAttached(true); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (pager != null) { + if (adapterObserver.isAttached()) { + pager.getAdapter().unregisterDataSetObserver(adapterObserver); + adapterObserver.setAttached(false); + } + } + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + currentPosition = savedState.currentPosition; + if (currentPosition != 0 && tabsContainer.getChildCount() > 0) { + notSelected(tabsContainer.getChildAt(0)); + selected(tabsContainer.getChildAt(currentPosition)); + } + requestLayout(); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState savedState = new SavedState(superState); + savedState.currentPosition = currentPosition; + return savedState; + } + + static class SavedState extends BaseSavedState { + int currentPosition; + + public SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPosition = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(currentPosition); + } + + public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public int getIndicatorColor() { + return this.indicatorColor; + } + + public int getIndicatorHeight() { + return indicatorHeight; + } + + public int getUnderlineColor() { + return underlineColor; + } + + public int getDividerColor() { + return dividerColor; + } + + public int getDividerWidth() { + return dividerWidth; + } + + public int getUnderlineHeight() { + return underlineHeight; + } + + public int getDividerPadding() { + return dividerPadding; + } + + public int getScrollOffset() { + return scrollOffset; + } + + public boolean getShouldExpand() { + return shouldExpand; + } + + public int getTextSize() { + return tabTextSize; + } + + public boolean isTextAllCaps() { + return textAllCaps; + } + + public ColorStateList getTextColor() { + return tabTextColor; + } + + public int getTabBackground() { + return tabBackgroundResId; + } + + public int getTabPaddingLeftRight() { + return tabPadding; + } + + public void setIndicatorColor(int indicatorColor) { + this.indicatorColor = indicatorColor; + invalidate(); + } + + public void setIndicatorColorResource(int resId) { + this.indicatorColor = getResources().getColor(resId); + invalidate(); + } + + public void setIndicatorHeight(int indicatorLineHeightPx) { + this.indicatorHeight = indicatorLineHeightPx; + invalidate(); + } + + public void setUnderlineColor(int underlineColor) { + this.underlineColor = underlineColor; + invalidate(); + } + + public void setUnderlineColorResource(int resId) { + this.underlineColor = getResources().getColor(resId); + invalidate(); + } + + public void setDividerColor(int dividerColor) { + this.dividerColor = dividerColor; + invalidate(); + } + + public void setDividerColorResource(int resId) { + this.dividerColor = getResources().getColor(resId); + invalidate(); + } + + public void setDividerWidth(int dividerWidthPx) { + this.dividerWidth = dividerWidthPx; + invalidate(); + } + + public void setUnderlineHeight(int underlineHeightPx) { + this.underlineHeight = underlineHeightPx; + invalidate(); + } + + public void setDividerPadding(int dividerPaddingPx) { + this.dividerPadding = dividerPaddingPx; + invalidate(); + } + + public void setScrollOffset(int scrollOffsetPx) { + this.scrollOffset = scrollOffsetPx; + invalidate(); + } + + public void setShouldExpand(boolean shouldExpand) { + this.shouldExpand = shouldExpand; + if (pager != null) { + requestLayout(); + } + } + + public void setAllCaps(boolean textAllCaps) { + this.textAllCaps = textAllCaps; + } + + public void setTextSize(int textSizePx) { + this.tabTextSize = textSizePx; + updateTabStyles(); + } + + public void setTextColor(int textColor) { + setTextColor(new ColorStateList(new int[][]{new int[]{}}, new int[]{textColor})); + } + + public void setTextColor(ColorStateList colorStateList) { + this.tabTextColor = colorStateList; + updateTabStyles(); + } + + public void setTextColorResource(int resId) { + setTextColor(getResources().getColor(resId)); + } + + public void setTextColorStateListResource(int resId) { + setTextColor(getResources().getColorStateList(resId)); + } + + public void setTypeface(Typeface typeface, int style) { + this.tabTypeface = typeface; + this.tabTypefaceSelectedStyle = style; + updateTabStyles(); + } + + public void setTabBackground(int resId) { + this.tabBackgroundResId = resId; + } + + public void setTabPaddingLeftRight(int paddingPx) { + this.tabPadding = paddingPx; + updateTabStyles(); + } +}
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java b/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java new file mode 100644 index 00000000..4b477f9c --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; + +public class RemoteCNPreference extends DialogPreference { + + + private Spinner mSpinner; + private EditText mEditText; + private int mDNType; + private String mDn; + private TextView mRemoteTLSNote; + //private ScrollView mScrollView; + + public RemoteCNPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setDialogLayoutResource(R.layout.tlsremote); + + } + + @Override + protected void onBindDialogView(View view) { + + super.onBindDialogView(view); + + mEditText = (EditText) view.findViewById(R.id.tlsremotecn); + mSpinner = (Spinner) view.findViewById(R.id.x509verifytype); + mRemoteTLSNote = (TextView) view.findViewById(R.id.tlsremotenote); + //mScrollView = (ScrollView) view.findViewById(R.id.tlsremotescroll); + if(mDn!=null) + mEditText.setText(mDn); + + populateSpinner(); + + } + + + + public String getCNText() { + return mDn; + } + + public int getAuthtype() { + return mDNType; + } + + public void setDN(String dn) { + mDn = dn; + if(mEditText!=null) + mEditText.setText(dn); + } + + public void setAuthType(int x509authtype) { + mDNType = x509authtype; + if (mSpinner!=null) + populateSpinner(); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult) { + String dn = mEditText.getText().toString(); + int authtype = getAuthTypeFromSpinner(); + if (callChangeListener(new Pair<Integer, String>(authtype, dn))) { + mDn = dn; + mDNType = authtype; + } + } + } + + private void populateSpinner() { + ArrayAdapter<String> authtypes = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item); + authtypes.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + authtypes.add(getContext().getString(R.string.complete_dn)); + authtypes.add(getContext().getString(R.string.rdn)); + authtypes.add(getContext().getString(R.string.rdn_prefix)); + if ((mDNType == VpnProfile.X509_VERIFY_TLSREMOTE || mDNType == VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING) + && !(mDn==null || "".equals(mDn))) { + authtypes.add(getContext().getString(R.string.tls_remote_deprecated)); + mRemoteTLSNote.setVisibility(View.VISIBLE); + } else { + mRemoteTLSNote.setVisibility(View.GONE); + } + mSpinner.setAdapter(authtypes); + mSpinner.setSelection(getSpinnerPositionFromAuthTYPE()); + } + + private int getSpinnerPositionFromAuthTYPE() { + switch (mDNType) { + case VpnProfile.X509_VERIFY_TLSREMOTE_DN: + return 0; + case VpnProfile.X509_VERIFY_TLSREMOTE_RDN: + return 1; + case VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX: + return 2; + case VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING: + case VpnProfile.X509_VERIFY_TLSREMOTE: + if (mDn==null || "".equals(mDn)) + return 1; + else + return 3; + + + default: + return 0; + } + } + + private int getAuthTypeFromSpinner() { + int pos = mSpinner.getSelectedItemPosition(); + switch (pos) { + case 0: + return VpnProfile.X509_VERIFY_TLSREMOTE_DN; + case 1: + return VpnProfile.X509_VERIFY_TLSREMOTE_RDN; + case 2: + return VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX; + case 3: + // This is the tls-remote entry, only visible if mDntype is a + // tls-remote type + return mDNType; + default: + return VpnProfile.X509_VERIFY_TLSREMOTE; + } + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java b/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java new file mode 100644 index 00000000..38bb54b5 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.annotation.StringRes; +import android.support.v4n.app.FragmentStatePagerAdapter; + +import java.util.Vector; + +import de.blinkt.openvpn.activities.MainActivity; + +/** +* Created by arne on 18.11.14. +*/ +public class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + + private final Resources res; + private Bundle mFragmentArguments; + + public void setFragmentArgs(Bundle fragmentArguments) { + mFragmentArguments = fragmentArguments; + } + + static class Tab { + public Class<? extends Fragment> fragmentClass; + String mName; + + public Tab(Class<? extends Fragment> fClass, String name){ + mName = name; + fragmentClass = fClass; + } + + } + + + private Vector<Tab> mTabs = new Vector<Tab>(); + + public ScreenSlidePagerAdapter(FragmentManager fm, Context c) { + super(fm); + res = c.getResources(); + } + + @Override + public Fragment getItem(int position) { + try { + Fragment fragment = mTabs.get(position).fragmentClass.newInstance(); + if (mFragmentArguments!=null) + fragment.setArguments(mFragmentArguments); + return fragment; + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabs.get(position).mName; + } + + @Override + public int getCount() { + return mTabs.size(); + } + + public void addTab(@StringRes int name, Class<? extends Fragment> fragmentClass) { + mTabs.add(new Tab(fragmentClass, res.getString(name))); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java b/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java new file mode 100644 index 00000000..347ce708 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.ViewConfiguration; +import android.widget.SeekBar; + +public class SeekBarTicks extends SeekBar { + private Paint mTickPaint; + private float mTickHeight; + + private float tickHeightRatio = 0.6f; + + public SeekBarTicks(Context context, AttributeSet attrs) { + super (context, attrs); + + initTicks (context, attrs, android.R.attr.seekBarStyle); + } + + + public SeekBarTicks(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + initTicks (context, attrs, defStyle); + + /*mTickHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + tickHeightDP, + ctx.getResources().getDisplayMetrics()); */ + } + + private void initTicks(Context context, AttributeSet attrs, int defStyle) { + TypedArray a = context.obtainStyledAttributes(attrs, + new int[] { android.R.attr.secondaryProgress }, defStyle, 0); + + mTickPaint = new Paint(); + //noinspection deprecation + mTickPaint.setColor( context.getResources().getColor(android.R.color.black)); + a.recycle(); + } + + + @Override + protected synchronized void onDraw(Canvas canvas) { + drawTicks(canvas); + super.onDraw(canvas); + } + + private void drawTicks(Canvas canvas) { + + final int available = getWidth() - getPaddingLeft() - getPaddingRight(); + final int availableHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + + int extrapadding = (int) ((availableHeight- (availableHeight * tickHeightRatio))/2); + + int tickSpacing = available / (getMax() ); + + for (int i = 1; i < getMax(); i++) { + final float x = getPaddingLeft() + i * tickSpacing; + + canvas.drawLine(x, getPaddingTop()+extrapadding, x, getHeight()-getPaddingBottom()-extrapadding, mTickPaint); + } + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java new file mode 100644 index 00000000..ea3b1c26 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.support.v4n.view.PagerAdapter; +import android.support.v4n.view.ViewPager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.HorizontalScrollView; +import android.widget.TextView; + +/** + * To be used with ViewPager to provide a tab indicator component which give constant feedback as to + * the user's scroll progress. + * <p> + * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call + * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for. + * <p> + * The colors can be customized in two ways. The first and simplest is to provide an array of colors + * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The + * alternative is via the {@link TabColorizer} interface which provides you complete control over + * which color is used for any individual position. + * <p> + * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, + * providing the layout ID of your custom layout. + */ +public class SlidingTabLayout extends HorizontalScrollView implements TabBarView { + + /** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(TabColorizer)}. + */ + public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} is selected. + */ + int getIndicatorColor(int position); + + /** + * @return return the color of the divider drawn to the right of {@code position}. + */ + int getDividerColor(int position); + + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + + private int mTitleOffset; + + private int mTabViewLayoutId; + private int mTabViewTextViewId; + + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private final SlidingTabStrip mTabStrip; + + public SlidingTabLayout(Context context) { + this(context, null); + } + + public SlidingTabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new SlidingTabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple custmisation then you can use + * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Sets the colors to be used for tab dividers. These colors are treated as a circular array. + * Providing one color will mean that all tabs are indicated with the same color. + */ + public void setDividerColors(int... colors) { + mTabStrip.setDividerColors(colors); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are + * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + mTabViewLayoutId = layoutResId; + mTabViewTextViewId = textViewId; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + if (viewPager != null) { + viewPager.setOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(Context context) { + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // If we're running on Honeycomb or newer, then we can use the Theme's + // selectableItemBackground to ensure that the View has a pressed state + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style + textView.setAllCaps(true); + } + + int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); + textView.setPadding(padding, padding, padding, padding); + + return textView; + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final View.OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + TextView tabTitleView = null; + + if (mTabViewLayoutId != 0) { + // If there is a custom tab view layout id set, try and inflate it + tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, + false); + tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); + } + + if (tabView == null) { + tabView = createDefaultTabView(getContext()); + } + + if (tabTitleView == null && TextView.class.isInstance(tabView)) { + tabTitleView = (TextView) tabView; + } + + tabTitleView.setText(adapter.getPageTitle(i)); + tabView.setOnClickListener(tabClickListener); + + mTabStrip.addView(tabView); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) + ? (int) (positionOffset * selectedTitle.getWidth()) + : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, + positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements View.OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java new file mode 100644 index 00000000..88bfb9a3 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +class SlidingTabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1; + private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20; + private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private final Paint mDividerPaint; + private final float mDividerHeight; + + private int mSelectedPosition; + private float mSelectionOffset; + + private SlidingTabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + SlidingTabStrip(Context context) { + this(context, null); + } + + SlidingTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + mDefaultTabColorizer.setDividerColors(setColorAlpha(themeForegroundColor, + DEFAULT_DIVIDER_COLOR_ALPHA)); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + + mDividerHeight = DEFAULT_DIVIDER_HEIGHT; + mDividerPaint = new Paint(); + mDividerPaint.setStrokeWidth((int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density)); + } + + void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void setDividerColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setDividerColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final int dividerHeightPx = (int) (Math.min(Math.max(0f, mDividerHeight), 1f) * height); + final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + + // Vertical separators between the titles + int separatorTop = (height - dividerHeightPx) / 2; + for (int i = 0; i < childCount - 1; i++) { + View child = getChildAt(i); + mDividerPaint.setColor(tabColorizer.getDividerColor(i)); + canvas.drawLine(child.getRight(), separatorTop, child.getRight(), + separatorTop + dividerHeightPx, mDividerPaint); + } + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { + private int[] mIndicatorColors; + private int[] mDividerColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + @Override + public final int getDividerColor(int position) { + return mDividerColors[position % mDividerColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + + void setDividerColors(int... colors) { + mDividerColors = colors; + } + } +}
\ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java b/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java new file mode 100644 index 00000000..71f03c03 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.support.v4n.view.ViewPager; + +/** + * Created by arne on 18.11.14. + */ +public interface TabBarView { + + void setViewPager(ViewPager mPager); +} |