diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5edb4ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000..bc6a634 Binary files /dev/null and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..7ac24c7 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..99202cc --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..23cb790 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..2d9b418 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 27 + defaultConfig { + applicationId "com.zzy.vpnservicedemo" + minSdkVersion 16 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support.constraint:constraint-layout:1.1.0' + implementation 'com.android.support:design:27.1.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.1' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/zzy/vpnservicedemo/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/zzy/vpnservicedemo/ExampleInstrumentedTest.java new file mode 100644 index 0000000..3b20faa --- /dev/null +++ b/app/src/androidTest/java/com/zzy/vpnservicedemo/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.zzy.vpnservicedemo; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.zzy.vpnservicedemo", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c5db1a0 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/zzy/vpnservicedemo/ByteBufferPool.java b/app/src/main/java/com/zzy/vpnservicedemo/ByteBufferPool.java new file mode 100755 index 0000000..1b48e65 --- /dev/null +++ b/app/src/main/java/com/zzy/vpnservicedemo/ByteBufferPool.java @@ -0,0 +1,60 @@ +/* +** Copyright 2015, Mohamed Naufal +** +** 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 com.zzy.vpnservicedemo; + +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class ByteBufferPool +{ + private static final int BUFFER_SIZE = 16384; + private static ConcurrentLinkedQueue pool = new ConcurrentLinkedQueue(); + + public static ByteBuffer acquire() + { + synchronized (pool) + { + ByteBuffer buffer = pool.poll(); + if (buffer == null) + { + buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); + } + buffer.clear(); + return buffer; + } + + } + + public static void release(ByteBuffer buffer) + { + synchronized (pool) + { + buffer.clear(); + // pool.offer(buffer); + } + + } + + public static void clear() + { + synchronized (pool) + { + pool.clear(); + } + } + +} diff --git a/app/src/main/java/com/zzy/vpnservicedemo/DemoService.java b/app/src/main/java/com/zzy/vpnservicedemo/DemoService.java new file mode 100755 index 0000000..c909d0d --- /dev/null +++ b/app/src/main/java/com/zzy/vpnservicedemo/DemoService.java @@ -0,0 +1,231 @@ +package com.zzy.vpnservicedemo; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.VpnService; +import android.os.ParcelFileDescriptor; +import android.util.Log; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class DemoService extends VpnService +{ + + private static final String TAG = "DemoService"; + public static final String VPN_ADDRESS = "168.168.168.168"; + private static final String VPN_ROUTE = "0.0.0.0"; + private static final String VPN_DNS = "192.168.1.1"; + + public static final String BROADCAST_VPN_STATE = "com.vpn.status"; + public static final String BROADCAST_STOP_VPN = "com.vpn.stop"; + + private ParcelFileDescriptor vpnInterface = null; + private ExecutorService executorService; + private VPNRunnable vpnRunnable; + + + @Override + public void onCreate() + { + super.onCreate(); + + registerReceiver(stopReceiver, new IntentFilter(BROADCAST_STOP_VPN)); + if(setupVPN()) { + + sendBroadcast(new Intent(BROADCAST_VPN_STATE).putExtra("running", true)); + vpnRunnable = new VPNRunnable(vpnInterface); + executorService = Executors.newFixedThreadPool(1); + executorService.submit(vpnRunnable); + } + } + + private boolean setupVPN() + { + try + { + if (vpnInterface == null) + { + Builder builder = new Builder(); + builder.addAddress(VPN_ADDRESS, 24); + builder.addRoute(VPN_ROUTE, 0); + + Intent configure = new Intent(this, MainActivity.class); + PendingIntent pi = PendingIntent.getActivity(this, 0, configure, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setConfigureIntent(pi); + + vpnInterface = builder.setSession(getString(R.string.app_name)).establish(); + + } + + return true; + } + catch (Exception e) + { + e.printStackTrace(); + } + + return false; + } + + private void stopVpn() + { + + if(vpnRunnable !=null) { + vpnRunnable.stop(); + } + if(vpnInterface !=null) { + try { + vpnInterface.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + vpnInterface = null; + vpnRunnable = null; + executorService = null; + + sendBroadcast(new Intent(BROADCAST_VPN_STATE).putExtra("running", false)); + } + + + private BroadcastReceiver stopReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + if (intent == null || intent.getAction() == null) + { + return; + } + + if (BROADCAST_STOP_VPN.equals(intent.getAction())) + { + onRevoke(); + stopVpn(); + + } + } + }; + + @Override + public void onDestroy() + { + super.onDestroy(); + stopVpn(); + unregisterReceiver(stopReceiver); + } + + private static class VPNRunnable implements Runnable + { + private static final String TAG = VPNRunnable.class.getSimpleName(); + ParcelFileDescriptor vpnInterface; + private boolean isStop; + + public VPNRunnable(ParcelFileDescriptor vpnInterface) + { + isStop = false; + this.vpnInterface = vpnInterface; + } + + public void stop() + { + isStop = true; + } + + @Override + public void run() + { + FileChannel vpnInput = new FileInputStream(vpnInterface.getFileDescriptor()).getChannel(); + FileChannel vpnOutput = new FileOutputStream(vpnInterface.getFileDescriptor()).getChannel(); + + ByteBuffer bufferToNetwork = null; + while (true) + { + if(isStop) + { + vpnInterface = null; + break; + } + + if (bufferToNetwork != null) + { + bufferToNetwork.clear(); + } + else + { + bufferToNetwork = ByteBufferPool.acquire(); + } + + int readBytes = 0; + try { + readBytes = vpnInput.read(bufferToNetwork); + } catch (IOException e) { + e.printStackTrace(); + } + + + if (readBytes > 0) + { + bufferToNetwork.flip(); + Packet packet = null; + try + { + packet = new Packet(bufferToNetwork, false); + } + catch (UnknownHostException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + String sIp = null; + if(packet.ip4Header.destinationAddress !=null) + { + sIp = packet.ip4Header.destinationAddress.getHostAddress(); + } + + + if (packet.isUDP()) + { + Log.i(TAG,"udp address:" + packet.ip4Header.sourceAddress.getHostAddress() + " udp port:" + + packet.udpHeader.sourcePort + " des:" + sIp + " des port:" + packet.udpHeader.destinationPort); + + } + else if (packet.isTCP()) + { + + Log.i(TAG,"tcp address:" + packet.ip4Header.sourceAddress.getHostAddress() + "tcp port:" + + packet.tcpHeader.sourcePort + " des:" + sIp + " des port:" + packet.tcpHeader.destinationPort); + + } + else if (packet.isPing()) + { + Log.w(TAG, packet.ip4Header.toString()); + } + else + { + Log.w(TAG, "Unknown packet type"); + Log.w(TAG, packet.ip4Header.toString()); + } + } + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + } + + } + } +} diff --git a/app/src/main/java/com/zzy/vpnservicedemo/MainActivity.java b/app/src/main/java/com/zzy/vpnservicedemo/MainActivity.java new file mode 100644 index 0000000..2d09197 --- /dev/null +++ b/app/src/main/java/com/zzy/vpnservicedemo/MainActivity.java @@ -0,0 +1,148 @@ +package com.zzy.vpnservicedemo; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.VpnService; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Button; + +public class MainActivity extends AppCompatActivity { + + private static final int VPN_REQUEST_CODE = 0x0F; + private Button btnStart; + private boolean isStart; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + } + }); + + registerReceiver(vpnStateReceiver, new IntentFilter(DemoService.BROADCAST_VPN_STATE)); + btnStart = findViewById(R.id.btnStart); + btnStart.setText("start"); + btnStart.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(!isStart) { + startVPN(); + }else{ + sendBroadcast(new Intent(DemoService.BROADCAST_STOP_VPN)); + } + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } + + private Handler handler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + return false; + } + }); + + private Runnable runnable = new Runnable() { + @Override + public void run() { + + stopService(new Intent(MainActivity.this, DemoService.class)); + } + }; + + + private BroadcastReceiver vpnStateReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + if (DemoService.BROADCAST_VPN_STATE.equals(intent.getAction())) + { + if (intent.getBooleanExtra("running", false)) + { + isStart = true; + btnStart.setText("stop"); + } + else + { + isStart =false; + btnStart.setText("start"); + handler.postDelayed(runnable,200); + } + } + } + }; + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK) + { + startService(new Intent(this, DemoService.class)); + } + } + + private void startVPN() + { + Intent vpnIntent = VpnService.prepare(this); + if (vpnIntent != null) + { + startActivityForResult(vpnIntent, VPN_REQUEST_CODE); + } + else + { + onActivityResult(VPN_REQUEST_CODE, RESULT_OK, null); + } + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + handler.removeCallbacks(runnable); + unregisterReceiver(vpnStateReceiver); + } + +} diff --git a/app/src/main/java/com/zzy/vpnservicedemo/Packet.java b/app/src/main/java/com/zzy/vpnservicedemo/Packet.java new file mode 100755 index 0000000..21ac732 --- /dev/null +++ b/app/src/main/java/com/zzy/vpnservicedemo/Packet.java @@ -0,0 +1,579 @@ +/* +** Copyright 2015, Mohamed Naufal +** +** 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 com.zzy.vpnservicedemo; + +import android.util.Log; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; + +/** + * Representation of an IP Packet + */ +// TODO: Reduce public mutability +public class Packet +{ + public static final int IP4_HEADER_SIZE = 20; + public static final int TCP_HEADER_SIZE = 20; + public static final int UDP_HEADER_SIZE = 8; + + public IP4Header ip4Header; + public TCPHeader tcpHeader; + public UDPHeader udpHeader; + public ByteBuffer backingBuffer; + + private boolean isTCP; + private boolean isUDP; + private boolean isPing; + private boolean bForbin=false; + private static final String TAG = "Packet"; + + public Packet(ByteBuffer buffer, boolean bForbin) throws UnknownHostException { + this.ip4Header = new IP4Header(buffer); + if (this.ip4Header.protocol == IP4Header.TransportProtocol.TCP) { + this.tcpHeader = new TCPHeader(buffer); + this.isTCP = true; + } else if (ip4Header.protocol == IP4Header.TransportProtocol.UDP) { + this.udpHeader = new UDPHeader(buffer); + this.isUDP = true; + } + else if (ip4Header.protocol == IP4Header.TransportProtocol.PING) { + this.isPing = true; + } + this.backingBuffer = buffer; + this.bForbin = bForbin; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("Packet{"); + sb.append("ip4Header=").append(ip4Header); + if (isTCP) sb.append(", tcpHeader=").append(tcpHeader); + else if (isUDP) sb.append(", udpHeader=").append(udpHeader); + sb.append(", payloadSize=").append(backingBuffer.limit() - backingBuffer.position()); + sb.append('}'); + return sb.toString(); + } + + public void setForbin() + { + bForbin = true; + } + + public boolean isForbin() + { + return bForbin; + } + + public boolean isTCP() + { + return isTCP; + } + + public boolean isUDP() + { + return isUDP; + } + + public boolean isPing() + { + return isPing; + } + + public void swapSourceAndDestination() + { + InetAddress newSourceAddress = ip4Header.destinationAddress; + ip4Header.destinationAddress = ip4Header.sourceAddress; + ip4Header.sourceAddress = newSourceAddress; + + if (isUDP) + { + int newSourcePort = udpHeader.destinationPort; + udpHeader.destinationPort = udpHeader.sourcePort; + udpHeader.sourcePort = newSourcePort; + } + else if (isTCP) + { + int newSourcePort = tcpHeader.destinationPort; + tcpHeader.destinationPort = tcpHeader.sourcePort; + tcpHeader.sourcePort = newSourcePort; + } + } + + public void updateTCPBuffer(ByteBuffer buffer, byte flags, long sequenceNum, long ackNum, int payloadSize) + { + //buffer.position(0); + buffer.clear(); + try + { + fillHeader(buffer); + backingBuffer = buffer; + + tcpHeader.flags = flags; + backingBuffer.put(IP4_HEADER_SIZE + 13, flags); + + tcpHeader.sequenceNumber = sequenceNum; + backingBuffer.putInt(IP4_HEADER_SIZE + 4, (int) sequenceNum); + + tcpHeader.acknowledgementNumber = ackNum; + backingBuffer.putInt(IP4_HEADER_SIZE + 8, (int) ackNum); + + // Reset header size, since we don't need options + byte dataOffset = (byte) (TCP_HEADER_SIZE << 2); + tcpHeader.dataOffsetAndReserved = dataOffset; + backingBuffer.put(IP4_HEADER_SIZE + 12, dataOffset); + + updateTCPChecksum(payloadSize); + + int ip4TotalLength = IP4_HEADER_SIZE + TCP_HEADER_SIZE + payloadSize; + backingBuffer.putShort(2, (short) ip4TotalLength); + ip4Header.totalLength = ip4TotalLength; + + updateIP4Checksum(); + + boolean isSyn = tcpHeader.isSYN(); + boolean isRst = tcpHeader.isRST(); + boolean isFIN = tcpHeader.isFIN(); + boolean isAck = tcpHeader.isACK(); + boolean isPsh = tcpHeader.isPSH(); + + Log.i(TAG,"fillHeader: sourceport" + tcpHeader.sourcePort + +" destinationPort:"+ tcpHeader.destinationPort + +" sourceAddress:"+ip4Header.sourceAddress.getHostAddress() + +" destinationAddress:"+ ip4Header.destinationAddress.getHostAddress() + +" sequenceNumber:"+tcpHeader.sequenceNumber + +" acknowledgementNumber:"+ tcpHeader.acknowledgementNumber + +" Syn:"+isSyn + +" Rst:"+isRst + +" FIN:"+isFIN + +" Ack:"+isAck + +" Psh:"+isPsh); + } + catch (Exception e) + { + int iLimit = buffer.limit(); + int iCap = buffer.capacity(); + int iposition = buffer.position(); + int iRemain = buffer.remaining(); + Log.i(TAG,"updateTCPBuffer limit:"+iLimit+" cap:"+iCap+" position:"+iposition+" remain:"+iRemain); + e.printStackTrace(); + } + } + + public void updateUDPBuffer(ByteBuffer buffer, int payloadSize) + { + buffer.position(0); + fillHeader(buffer); + backingBuffer = buffer; + + int udpTotalLength = UDP_HEADER_SIZE + payloadSize; + backingBuffer.putShort(IP4_HEADER_SIZE + 4, (short) udpTotalLength); + udpHeader.length = udpTotalLength; + + // Disable UDP checksum validation + backingBuffer.putShort(IP4_HEADER_SIZE + 6, (short) 0); + udpHeader.checksum = 0; + + int ip4TotalLength = IP4_HEADER_SIZE + udpTotalLength; + backingBuffer.putShort(2, (short) ip4TotalLength); + ip4Header.totalLength = ip4TotalLength; + + updateIP4Checksum(); + } + + private void updateIP4Checksum() + { + ByteBuffer buffer = backingBuffer.duplicate(); + buffer.position(0); + + // Clear previous checksum + buffer.putShort(10, (short) 0); + + int ipLength = ip4Header.headerLength; + int sum = 0; + while (ipLength > 0) + { + sum += BitUtils.getUnsignedShort(buffer.getShort()); + ipLength -= 2; + } + while (sum >> 16 > 0) + sum = (sum & 0xFFFF) + (sum >> 16); + + sum = ~sum; + ip4Header.headerChecksum = sum; + backingBuffer.putShort(10, (short) sum); + } + + private void updateTCPChecksum(int payloadSize) + { + int sum = 0; + int tcpLength = TCP_HEADER_SIZE + payloadSize; + + // Calculate pseudo-header checksum + ByteBuffer buffer = ByteBuffer.wrap(ip4Header.sourceAddress.getAddress()); + sum = BitUtils.getUnsignedShort(buffer.getShort()) + BitUtils.getUnsignedShort(buffer.getShort()); + + buffer = ByteBuffer.wrap(ip4Header.destinationAddress.getAddress()); + sum += BitUtils.getUnsignedShort(buffer.getShort()) + BitUtils.getUnsignedShort(buffer.getShort()); + + sum += IP4Header.TransportProtocol.TCP.getNumber() + tcpLength; + + buffer = backingBuffer.duplicate(); + // Clear previous checksum + buffer.putShort(IP4_HEADER_SIZE + 16, (short) 0); + + // Calculate TCP segment checksum + buffer.position(IP4_HEADER_SIZE); + while (tcpLength > 1) + { + sum += BitUtils.getUnsignedShort(buffer.getShort()); + tcpLength -= 2; + } + if (tcpLength > 0) + sum += BitUtils.getUnsignedByte(buffer.get()) << 8; + + while (sum >> 16 > 0) + sum = (sum & 0xFFFF) + (sum >> 16); + + sum = ~sum; + tcpHeader.checksum = sum; + backingBuffer.putShort(IP4_HEADER_SIZE + 16, (short) sum); + } + + private void fillHeader(ByteBuffer buffer) + { + ip4Header.fillHeader(buffer); + if (isUDP) + udpHeader.fillHeader(buffer); + else if (isTCP) + tcpHeader.fillHeader(buffer); + } + + public static class IP4Header + { + public byte version; + public byte IHL; + public int headerLength; + public short typeOfService; + public int totalLength; + + public int identificationAndFlagsAndFragmentOffset; + + public short TTL; + private short protocolNum; + public TransportProtocol protocol; + public int headerChecksum; + + public InetAddress sourceAddress; + public InetAddress destinationAddress; + + public int iSourceAddress; + public int iDestinationAddress; + + public int optionsAndPadding; + + public enum TransportProtocol + { + TCP(6), + UDP(17), + PING(1), + Other(0xFF); + + private int protocolNumber; + + TransportProtocol(int protocolNumber) + { + this.protocolNumber = protocolNumber; + } + + private static TransportProtocol numberToEnum(int protocolNumber) + { + if (protocolNumber == 6) + { + return TCP; + } + else if (protocolNumber == 17) + { + return UDP; + } + else if (protocolNumber == 1) + { + return PING; + } + else + { + Log.i(TAG,"numberToEnum protocolNumber:"+protocolNumber); + return Other; + } + } + + public int getNumber() + { + return this.protocolNumber; + } + } + + private IP4Header(ByteBuffer buffer) throws UnknownHostException + { + byte versionAndIHL = buffer.get(); + this.version = (byte) (versionAndIHL >> 4); + this.IHL = (byte) (versionAndIHL & 0x0F); + this.headerLength = this.IHL << 2; + + this.typeOfService = BitUtils.getUnsignedByte(buffer.get()); + this.totalLength = BitUtils.getUnsignedShort(buffer.getShort()); + + this.identificationAndFlagsAndFragmentOffset = buffer.getInt(); + + this.TTL = BitUtils.getUnsignedByte(buffer.get()); + this.protocolNum = BitUtils.getUnsignedByte(buffer.get()); + this.protocol = TransportProtocol.numberToEnum(protocolNum); + this.headerChecksum = BitUtils.getUnsignedShort(buffer.getShort()); + + byte[] addressBytes = new byte[4]; + buffer.get(addressBytes, 0, 4); + this.sourceAddress = InetAddress.getByAddress(addressBytes); + this.iSourceAddress = bytesToInt(addressBytes); + + buffer.get(addressBytes, 0, 4); + this.destinationAddress = InetAddress.getByAddress(addressBytes); + this.iDestinationAddress = bytesToInt(addressBytes); + + //this.optionsAndPadding = buffer.getInt(); + } + + public static int bytesToInt(byte[] bytes) + { + int addr = bytes[3] & 0xFF; + addr |= ((bytes[2] << 8) & 0xFF00); + addr |= ((bytes[1] << 16) & 0xFF0000); + addr |= ((bytes[0] << 24) & 0xFF000000); + return addr; + } + + public void fillHeader(ByteBuffer buffer) + { + buffer.put((byte) (this.version << 4 | this.IHL)); + buffer.put((byte) this.typeOfService); + buffer.putShort((short) this.totalLength); + + buffer.putInt(this.identificationAndFlagsAndFragmentOffset); + + buffer.put((byte) this.TTL); + buffer.put((byte) this.protocol.getNumber()); + buffer.putShort((short) this.headerChecksum); + + buffer.put(this.sourceAddress.getAddress()); + buffer.put(this.destinationAddress.getAddress()); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("IP4Header{"); + sb.append("version=").append(version); + sb.append(", IHL=").append(IHL); + sb.append(", typeOfService=").append(typeOfService); + sb.append(", totalLength=").append(totalLength); + sb.append(", identificationAndFlagsAndFragmentOffset=").append(identificationAndFlagsAndFragmentOffset); + sb.append(", TTL=").append(TTL); + sb.append(", protocol=").append(protocolNum).append(":").append(protocol); + sb.append(", headerChecksum=").append(headerChecksum); + sb.append(", sourceAddress=").append(sourceAddress.getHostAddress()); + sb.append(", destinationAddress=").append(destinationAddress.getHostAddress()); + sb.append('}'); + return sb.toString(); + } + } + + public static class TCPHeader + { + public static final int FIN = 0x01; + public static final int SYN = 0x02; + public static final int RST = 0x04; + public static final int PSH = 0x08; + public static final int ACK = 0x10; + public static final int URG = 0x20; + + public int sourcePort; + public int destinationPort; + + public long sequenceNumber; + public long acknowledgementNumber; + + public byte dataOffsetAndReserved; + public int headerLength; + public byte flags; + public int window; + + public int checksum; + public int urgentPointer; + + public byte[] optionsAndPadding; + + private TCPHeader(ByteBuffer buffer) + { + this.sourcePort = BitUtils.getUnsignedShort(buffer.getShort()); + this.destinationPort = BitUtils.getUnsignedShort(buffer.getShort()); + + this.sequenceNumber = BitUtils.getUnsignedInt(buffer.getInt()); + this.acknowledgementNumber = BitUtils.getUnsignedInt(buffer.getInt()); + + this.dataOffsetAndReserved = buffer.get(); + this.headerLength = (this.dataOffsetAndReserved & 0xF0) >> 2; + this.flags = buffer.get(); + this.window = BitUtils.getUnsignedShort(buffer.getShort()); + + this.checksum = BitUtils.getUnsignedShort(buffer.getShort()); + this.urgentPointer = BitUtils.getUnsignedShort(buffer.getShort()); + + int optionsLength = this.headerLength - TCP_HEADER_SIZE; + if (optionsLength > 0) + { + optionsAndPadding = new byte[optionsLength]; + buffer.get(optionsAndPadding, 0, optionsLength); + } + } + + public boolean isFIN() + { + return (flags & FIN) == FIN; + } + + public boolean isSYN() + { + return (flags & SYN) == SYN; + } + + public boolean isRST() + { + return (flags & RST) == RST; + } + + public boolean isPSH() + { + return (flags & PSH) == PSH; + } + + public boolean isACK() + { + return (flags & ACK) == ACK; + } + + public boolean isURG() + { + return (flags & URG) == URG; + } + + private void fillHeader(ByteBuffer buffer) + { + buffer.putShort((short) sourcePort); + buffer.putShort((short) destinationPort); + + buffer.putInt((int) sequenceNumber); + buffer.putInt((int) acknowledgementNumber); + + buffer.put(dataOffsetAndReserved); + buffer.put(flags); + buffer.putShort((short) window); + + buffer.putShort((short) checksum); + buffer.putShort((short) urgentPointer); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("TCPHeader{"); + sb.append("sourcePort=").append(sourcePort); + sb.append(", destinationPort=").append(destinationPort); + sb.append(", sequenceNumber=").append(sequenceNumber); + sb.append(", acknowledgementNumber=").append(acknowledgementNumber); + sb.append(", headerLength=").append(headerLength); + sb.append(", window=").append(window); + sb.append(", checksum=").append(checksum); + sb.append(", flags="); + if (isFIN()) sb.append(" FIN"); + if (isSYN()) sb.append(" SYN"); + if (isRST()) sb.append(" RST"); + if (isPSH()) sb.append(" PSH"); + if (isACK()) sb.append(" ACK"); + if (isURG()) sb.append(" URG"); + sb.append('}'); + return sb.toString(); + } + } + + public static class UDPHeader + { + public int sourcePort; + public int destinationPort; + + public int length; + public int checksum; + + private UDPHeader(ByteBuffer buffer) + { + this.sourcePort = BitUtils.getUnsignedShort(buffer.getShort()); + this.destinationPort = BitUtils.getUnsignedShort(buffer.getShort()); + + this.length = BitUtils.getUnsignedShort(buffer.getShort()); + this.checksum = BitUtils.getUnsignedShort(buffer.getShort()); + } + + private void fillHeader(ByteBuffer buffer) + { + buffer.putShort((short) this.sourcePort); + buffer.putShort((short) this.destinationPort); + + buffer.putShort((short) this.length); + buffer.putShort((short) this.checksum); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("UDPHeader{"); + sb.append("sourcePort=").append(sourcePort); + sb.append(", destinationPort=").append(destinationPort); + sb.append(", length=").append(length); + sb.append(", checksum=").append(checksum); + sb.append('}'); + return sb.toString(); + } + } + + private static class BitUtils + { + private static short getUnsignedByte(byte value) + { + return (short)(value & 0xFF); + } + + private static int getUnsignedShort(short value) + { + return value & 0xFFFF; + } + + private static long getUnsignedInt(int value) + { + return value & 0xFFFFFFFFL; + } + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..eed4d89 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..abbbeef --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,26 @@ + + + + + +