summaryrefslogtreecommitdiff
path: root/main/src/ui/java
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/ui/java')
-rw-r--r--main/src/ui/java/android/support/v4n/app/FragmentStatePagerAdapter.java232
-rw-r--r--main/src/ui/java/android/support/v4n/view/PagerAdapter.java320
-rw-r--r--main/src/ui/java/android/support/v4n/view/ViewPager.java2895
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/OpenVPNTileService.java151
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/BaseActivity.java43
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.java847
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/CreateShortcuts.java159
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/FileSelect.java257
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/LogWindow.java37
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/MainActivity.java131
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/OpenSSLSpeed.java193
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/activities/VPNPreferences.java244
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/AboutFragment.java333
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java413
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FaqFragment.java216
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FaqViewAdapter.java132
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/FileSelectionFragment.java325
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/GeneralSettings.java190
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/GraphFragment.java400
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/InlineFileTab.java70
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/KeyChainSettingsFragment.kt262
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/LogFragment.java694
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/OpenVpnPreferencesFragment.java53
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/SendDumpFragment.java128
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt323
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Authentication.java238
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Basic.java227
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.java101
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Fragment.java35
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_IP.java137
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Obscure.java212
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Routing.java109
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Settings_UserEditable.java63
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/ShowConfigFragment.java134
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/Utils.java288
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java636
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java39
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java189
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java164
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java732
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java146
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java79
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java73
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java314
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java207
-rw-r--r--main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java16
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);
+}