diff --git a/.travis.yml b/.travis.yml index 0ac44119..4be7cf06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,17 +3,15 @@ jdk: - oraclejdk8 android: components: - - tools - - build-tools-28.0.3 - - android-29 - - extra-android-m2repository + - build-tools-30.0.3 + - android-30 env: global: - secure: "em+bEb3nWLV7f+Dt4y9/OH2Lpf6mscT9E52kXhSEm/PiFpTtzlx7BgHGlpsYVwsMf0e7x2AdF/yzq4c7ugoMqWmFXMthgFUKcLTXqW/IeARGz3smMXnyScRoaFElDm0Q33fxiqDlU7UVA4j1FysR5GqTU10MasJgGpoyTthRQvra0LU8zwI4IMiASfa8Tij5LclsbaTpcgB4TdXtBmbpKQAUzjvV/ZfLsYQ950C0rbVKk9T1xtc8d5BTbggwHWbMMqGn6RFvSCQioeI/Q6Daak6zDglUY+JE/1BnzlGzoba9C7TLZV4+TQkmB5kc6FtKqSXycjwJSKm3wQFYOzJrLMPqMtlmaAeTEs5yBIYtIIAiID+hbeUKvWTpoUxwUYz7s7XLikJRdxe7iBlhdy3qv+FdGOJjV9g/xeSVXISvXzcKp2sNdU+FABBBDAA2CKWrZOXuzPAh8iYxLnt649hQFAw99ylkn75+LpJgq7gRUO87Fde0Zw3vYhLIS8UH3OBtggV1yrYHuKXWUyfkiA/xcm+NSFgIXJzGvYlOZWS+uZvXeh7wJz5CF0t3m6emBoqX6JAXQ/t9SgwGX8lwbLPK3HiYBnTMEDxb6qpIiYa8E8tfudA1YNyeszG8mZtiErJf9YleagACuASwPFsLb8Bg9BNJX5X89+VrbzPbncZD9Tc=" before_install: - - yes | sdkmanager "platforms;android-27" + - yes | sdkmanager "platforms;android-30" - echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- addons: diff --git a/app/build.gradle b/app/build.gradle index 24c9b554..a1a45ff2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,25 +5,24 @@ sonarqube { properties { property "sonar.projectName", "Port Authority" property "sonar.projectKey", "portauthority" - property "sonar.projectVersion", "1.0" + property "sonar.host.url", "http://192.168.1.3:9000" property "sonar.sourceEncoding", "UTF-8" - property "sonar.sources", "." property "sonar.exclusions", "build/**,**/*.png,**/*.psd" property "sonar.import_unknown_files", true - property "sonar.android.lint.report", "./build/outputs/lint-results.xml" + property "sonar.android.lint.report", "./build/reports/lint-results.xml" } } android { viewBinding.enabled = true - compileSdkVersion 29 - buildToolsVersion '28.0.3' + compileSdkVersion 30 + buildToolsVersion '30.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 29 - versionCode 61 - versionName "2.3.3" + targetSdkVersion 30 + versionCode 62 + versionName "2.4.0" applicationId "com.aaronjwood.portauthority" setProperty("archivesBaseName", "PortAuthority-$versionName") } @@ -67,12 +66,12 @@ android { } dependencies { - implementation 'com.android.support:support-v4:28.0.0' - implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.squareup.okhttp3:okhttp:3.12.9' // Anything past 3.12.x will break our Android 4 support! implementation 'jcifs:jcifs:1.3.17' implementation 'org.minidns:minidns-hla:0.3.2' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' testImplementation 'org.mockito:mockito-core:1.10.19' } diff --git a/app/src/main/java/com/aaronjwood/portauthority/activity/DnsActivity.java b/app/src/main/java/com/aaronjwood/portauthority/activity/DnsActivity.java index 8e9c649e..3c2895ad 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/activity/DnsActivity.java +++ b/app/src/main/java/com/aaronjwood/portauthority/activity/DnsActivity.java @@ -1,8 +1,7 @@ package com.aaronjwood.portauthority.activity; +import android.os.AsyncTask; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import android.widget.EditText; @@ -10,6 +9,9 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + import com.aaronjwood.portauthority.R; import com.aaronjwood.portauthority.async.DnsLookupAsyncTask; import com.aaronjwood.portauthority.response.DnsAsyncResponse; @@ -41,7 +43,7 @@ protected void onCreate(Bundle savedInstanceState) { } @Override - protected void onSaveInstanceState(Bundle savedState) { + protected void onSaveInstanceState(@NonNull Bundle savedState) { super.onSaveInstanceState(savedState); String recordData = dnsAnswer.getText().toString(); @@ -92,7 +94,7 @@ public void onClick(View view) { if (recordType != null) { String recordName = recordType.toString(); Toast.makeText(getApplicationContext(), getResources().getString(R.string.startingDnsLookup), Toast.LENGTH_SHORT).show(); - new DnsLookupAsyncTask(DnsActivity.this).execute(domain, recordName); + new DnsLookupAsyncTask(DnsActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, domain, recordName); } } }); diff --git a/app/src/main/java/com/aaronjwood/portauthority/activity/HostActivity.java b/app/src/main/java/com/aaronjwood/portauthority/activity/HostActivity.java index 784c3ddd..7a1b9567 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/activity/HostActivity.java +++ b/app/src/main/java/com/aaronjwood/portauthority/activity/HostActivity.java @@ -8,7 +8,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.v7.app.AppCompatActivity; import android.util.SparseArray; import android.view.View; import android.view.animation.AnimationUtils; @@ -20,6 +19,9 @@ import android.widget.NumberPicker; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + import com.aaronjwood.portauthority.R; import com.aaronjwood.portauthority.db.Database; import com.aaronjwood.portauthority.listener.ScanPortsListener; @@ -96,21 +98,13 @@ public void onPause() { portRangeDialog = null; } - /** - * Clean up - */ - @Override - protected void onDestroy() { - super.onDestroy(); - } - /** * Save the state of the activity * * @param savedState Data to save */ @Override - public void onSaveInstanceState(Bundle savedState) { + public void onSaveInstanceState(@NonNull Bundle savedState) { super.onSaveInstanceState(savedState); String[] savedList = ports.toArray(new String[0]); diff --git a/app/src/main/java/com/aaronjwood/portauthority/activity/LanHostActivity.java b/app/src/main/java/com/aaronjwood/portauthority/activity/LanHostActivity.java index d2219d9e..c08d288c 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/activity/LanHostActivity.java +++ b/app/src/main/java/com/aaronjwood/portauthority/activity/LanHostActivity.java @@ -9,6 +9,8 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; + import com.aaronjwood.portauthority.R; import com.aaronjwood.portauthority.listener.ScanPortsListener; import com.aaronjwood.portauthority.network.Host; @@ -61,7 +63,7 @@ protected void onCreate(Bundle savedInstanceState) { * @param savedState Data to save */ @Override - public void onSaveInstanceState(Bundle savedState) { + public void onSaveInstanceState(@NonNull Bundle savedState) { super.onSaveInstanceState(savedState); savedState.putSerializable("host", host); diff --git a/app/src/main/java/com/aaronjwood/portauthority/activity/MainActivity.java b/app/src/main/java/com/aaronjwood/portauthority/activity/MainActivity.java index e7b25c0d..df5ba3d4 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/activity/MainActivity.java +++ b/app/src/main/java/com/aaronjwood/portauthority/activity/MainActivity.java @@ -16,15 +16,11 @@ import android.database.sqlite.SQLiteException; import android.net.NetworkInfo; import android.net.wifi.WifiManager; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.AppCompatActivity; import android.view.ContextMenu; import android.view.MenuInflater; import android.view.MenuItem; @@ -41,6 +37,13 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; + import com.aaronjwood.portauthority.R; import com.aaronjwood.portauthority.adapter.HostAdapter; import com.aaronjwood.portauthority.async.DownloadAsyncTask; @@ -57,8 +60,6 @@ import com.aaronjwood.portauthority.utils.Errors; import com.aaronjwood.portauthority.utils.UserPreference; -import java.io.IOException; -import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; import java.nio.ByteBuffer; @@ -84,9 +85,9 @@ public final class MainActivity extends AppCompatActivity implements MainAsyncRe private Button discoverHostsBtn; private String discoverHostsStr; // Cache this so it's not looked up every time a host is found. private ProgressDialog scanProgressDialog; - private Handler signalHandler = new Handler(); + private final Handler signalHandler = new Handler(); private Handler scanHandler; - private IntentFilter intentFilter = new IntentFilter(); + private final IntentFilter intentFilter = new IntentFilter(); private HostAdapter hostAdapter; private List hosts = Collections.synchronizedList(new ArrayList<>()); private Database db; @@ -94,7 +95,7 @@ public final class MainActivity extends AppCompatActivity implements MainAsyncRe private DownloadAsyncTask portTask; private boolean sortAscending; - private BroadcastReceiver receiver = new BroadcastReceiver() { + private final BroadcastReceiver receiver = new BroadcastReceiver() { /** * Detect if a network connection has been lost or established @@ -162,15 +163,14 @@ private void ssidAccess(Context context) { } Activity activity = this; - String title = "Android 8-9 SSID Access"; - String message = "Android 8-9 requires coarse location permissions to read the SSID. " + - "If this is not something you're comfortable with just deny the request and go without the functionality."; + String version = "8-9"; + String message = getResources().getString(R.string.ssidCoarseMsg, version); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - title = "Android 10+ SSID Access"; - message = "Android 10+ requires fine location permissions to read the SSID. " + - "If this is not something you're comfortable with just deny the request and go without the functionality."; + version = "10+"; + message = getResources().getString(R.string.ssidFineMsg, version); } + String title = getResources().getString(R.string.ssidAccessTitle, version); new AlertDialog.Builder(activity, R.style.DialogTheme).setTitle(title) .setMessage(message) .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { @@ -205,18 +205,29 @@ public void checkDatabase() { } final MainActivity activity = this; - new AlertDialog.Builder(activity, R.style.DialogTheme).setTitle("Generate Database") - .setMessage("Do you want to create the OUI and port databases? " + - "This will download the official OUI list from Wireshark and port list from IANA. " + - "Note that you won't be able to resolve any MAC vendors or identify services without this data. " + - "You can always perform this from the menu later.") + new AlertDialog.Builder(activity, R.style.DialogTheme) + .setTitle(R.string.ouiDbTitle) + .setMessage(R.string.ouiDbMsg) .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> { dialogInterface.dismiss(); ouiTask = new DownloadOuisAsyncTask(db, new OuiParser(), activity); + ouiTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }) + .setNegativeButton(android.R.string.no, (dialogInterface, i) -> dialogInterface.cancel()) + .setIcon(android.R.drawable.ic_dialog_alert).show() + .setCanceledOnTouchOutside(false); + + new AlertDialog.Builder(activity, R.style.DialogTheme) + .setTitle(R.string.portDbTitle) + .setMessage(R.string.portDbMsg) + .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> { + dialogInterface.dismiss(); portTask = new DownloadPortDataAsyncTask(db, new PortParser(), activity); - ouiTask.execute(); - portTask.execute(); - }).setNegativeButton(android.R.string.no, (dialogInterface, i) -> dialogInterface.cancel()).setIcon(android.R.drawable.ic_dialog_alert).show().setCanceledOnTouchOutside(false); + portTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }) + .setNegativeButton(android.R.string.no, (dialogInterface, i) -> dialogInterface.cancel()) + .setIcon(android.R.drawable.ic_dialog_alert).show() + .setCanceledOnTouchOutside(false); } /** @@ -263,7 +274,7 @@ public void setupMac() { } catch (UnknownHostException | SocketException | Wireless.NoWifiManagerException e) { macAddress.setText(R.string.noWifiConnection); macVendor.setText(R.string.noWifiConnection); - } catch (IOException | SQLiteException | UnsupportedOperationException e) { + } catch (SQLiteException | UnsupportedOperationException e) { macVendor.setText(R.string.getMacVendorFailed); } catch (Wireless.NoWifiInterface e) { macAddress.setText(R.string.noWifiInterface); @@ -324,7 +335,7 @@ public void onClick(View v) { try { Integer ip = wifi.getInternalWifiIpAddress(Integer.class); - new ScanHostsAsyncTask(MainActivity.this, db).execute(ip, wifi.getInternalWifiSubnet(), UserPreference.getHostSocketTimeout(context)); + new ScanHostsAsyncTask(MainActivity.this, db).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, ip, wifi.getInternalWifiSubnet(), UserPreference.getHostSocketTimeout(context)); discoverHostsBtn.setAlpha(.3f); discoverHostsBtn.setEnabled(false); } catch (UnknownHostException | Wireless.NoWifiManagerException e) { @@ -385,6 +396,23 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMen public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); switch (item.getItemId()) { + case R.id.sortIp: + sortAscending = !sortAscending; + if (sortAscending) { + hostAdapter.sort((lhs, rhs) -> { + int leftIp = ByteBuffer.wrap(lhs.getAddress()).getInt(); + int rightIp = ByteBuffer.wrap(rhs.getAddress()).getInt(); + return rightIp - leftIp; + }); + } else { + hostAdapter.sort((lhs, rhs) -> { + int leftIp = ByteBuffer.wrap(lhs.getAddress()).getInt(); + int rightIp = ByteBuffer.wrap(rhs.getAddress()).getInt(); + return leftIp - rightIp; + }); + } + + return true; case R.id.sortHostname: if (sortAscending) { hostAdapter.sort((lhs, rhs) -> rhs.getHostname().toLowerCase().compareTo(lhs.getHostname().toLowerCase())); @@ -556,31 +584,27 @@ public void onItemClick(AdapterView parent, View view, int position, long id) wolDialog.setContentView(R.layout.wake_on_lan); wolDialog.show(); Button wakeUp = wolDialog.findViewById(R.id.wolWake); - wakeUp.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - try { - if (!wifi.isConnectedWifi()) { - Errors.showError(getApplicationContext(), getResources().getString(R.string.notConnectedLan)); - return; - } - } catch (Wireless.NoConnectivityManagerException e) { - Errors.showError(getApplicationContext(), getResources().getString(R.string.failedWifiManager)); - return; - } - - EditText ip = wolDialog.findViewById(R.id.wolIp); - EditText mac = wolDialog.findViewById(R.id.wolMac); - String ipVal = ip.getText().toString(); - String macVal = mac.getText().toString(); - if (ipVal.isEmpty() || macVal.isEmpty()) { + wakeUp.setOnClickListener(v -> { + try { + if (!wifi.isConnectedWifi()) { + Errors.showError(getApplicationContext(), getResources().getString(R.string.notConnectedLan)); return; } + } catch (Wireless.NoConnectivityManagerException e) { + Errors.showError(getApplicationContext(), getResources().getString(R.string.failedWifiManager)); + return; + } - new WolAsyncTask().execute(macVal, ipVal); - Toast.makeText(getApplicationContext(), String.format(getResources().getString(R.string.waking), ipVal), Toast.LENGTH_SHORT).show(); + EditText ip = wolDialog.findViewById(R.id.wolIp); + EditText mac = wolDialog.findViewById(R.id.wolMac); + String ipVal = ip.getText().toString(); + String macVal = mac.getText().toString(); + if (ipVal.isEmpty() || macVal.isEmpty()) { + return; } + + new WolAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, macVal, ipVal); + Toast.makeText(getApplicationContext(), String.format(getResources().getString(R.string.waking), ipVal), Toast.LENGTH_SHORT).show(); }); break; case 2: @@ -605,11 +629,11 @@ public void onItemClick(AdapterView parent, View view, int position, long id) switch (position) { case 0: ouiTask = new DownloadOuisAsyncTask(db, new OuiParser(), MainActivity.this); - ouiTask.execute(); + ouiTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); break; case 1: portTask = new DownloadPortDataAsyncTask(db, new PortParser(), MainActivity.this); - portTask.execute(); + portTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); break; case 2: startActivity(new Intent(MainActivity.this, PreferencesActivity.class)); @@ -700,7 +724,7 @@ public void onResume() { * @param savedState Data to save */ @Override - public void onSaveInstanceState(Bundle savedState) { + public void onSaveInstanceState(@NonNull Bundle savedState) { super.onSaveInstanceState(savedState); ListAdapter adapter = hostList.getAdapter(); @@ -745,14 +769,9 @@ public void processFinish(final Host h, final AtomicInteger i) { scanHandler.post(() -> { hosts.add(h); hostAdapter.sort((lhs, rhs) -> { - try { - int leftIp = ByteBuffer.wrap(InetAddress.getByName(lhs.getIp()).getAddress()).getInt(); - int rightIp = ByteBuffer.wrap(InetAddress.getByName(rhs.getIp()).getAddress()).getInt(); - - return leftIp - rightIp; - } catch (UnknownHostException ignored) { - return 0; - } + int leftIp = ByteBuffer.wrap(lhs.getAddress()).getInt(); + int rightIp = ByteBuffer.wrap(rhs.getAddress()).getInt(); + return leftIp - rightIp; }); discoverHostsBtn.setText(discoverHostsStr + " (" + hosts.size() + ")"); diff --git a/app/src/main/java/com/aaronjwood/portauthority/activity/WanHostActivity.java b/app/src/main/java/com/aaronjwood/portauthority/activity/WanHostActivity.java index 087756f9..a7257426 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/activity/WanHostActivity.java +++ b/app/src/main/java/com/aaronjwood/portauthority/activity/WanHostActivity.java @@ -35,15 +35,6 @@ protected void onCreate(Bundle savedInstanceState) { this.setupPortScan(); } - /** - * Clean up - */ - @Override - protected void onDestroy() { - super.onDestroy(); - UserPreference.saveLastUsedHostAddress(this, this.wanHost.getText().toString()); - } - /** * Event handler for when the well known port scan is initiated */ diff --git a/app/src/main/java/com/aaronjwood/portauthority/adapter/HostAdapter.java b/app/src/main/java/com/aaronjwood/portauthority/adapter/HostAdapter.java index a0d48a20..ecf4e715 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/adapter/HostAdapter.java +++ b/app/src/main/java/com/aaronjwood/portauthority/adapter/HostAdapter.java @@ -1,7 +1,7 @@ package com.aaronjwood.portauthority.adapter; import android.content.Context; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/com/aaronjwood/portauthority/async/DnsLookupAsyncTask.java b/app/src/main/java/com/aaronjwood/portauthority/async/DnsLookupAsyncTask.java index cde4d9c5..3eeef5cf 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/async/DnsLookupAsyncTask.java +++ b/app/src/main/java/com/aaronjwood/portauthority/async/DnsLookupAsyncTask.java @@ -1,7 +1,9 @@ package com.aaronjwood.portauthority.async; +import android.content.Context; import android.os.AsyncTask; +import com.aaronjwood.portauthority.R; import com.aaronjwood.portauthority.response.DnsAsyncResponse; import org.minidns.hla.ResolverApi; @@ -37,29 +39,32 @@ protected String doInBackground(String... params) { String domain = params[0]; String recordType = params[1]; ResolverResult result; + DnsAsyncResponse activity = delegate.get(); + Context ctx = (Context) activity; + try { Class dataClass = Record.TYPE.valueOf(recordType).getDataClass(); if (dataClass == null) { - return "Record type " + recordType + " not supported"; + return ctx.getResources().getString(R.string.unsuppRecType, recordType); } result = ResolverApi.INSTANCE.resolve(domain, dataClass); } catch (IOException e) { - return "Error performing lookup on type " + recordType + ": " + e.getMessage(); + return ctx.getResources().getString(R.string.lookupErr, recordType, e.getMessage()); } if (!result.wasSuccessful()) { - return "Lookup of type " + recordType + " failed with response code " + result.getResponseCode(); + return ctx.getResources().getString(R.string.lookupTypeFail, recordType, result.getResponseCode()); } Set answers = result.getAnswers(); if (answers.isEmpty()) { - return "No records found for type " + recordType; + return ctx.getResources().getString(R.string.noRecords, recordType); } StringBuilder out = new StringBuilder(); for (Data answer : answers) { - out.append(answer.toString()).append("\n\n"); + out.append(answer).append("\n\n"); } return out.toString(); diff --git a/app/src/main/java/com/aaronjwood/portauthority/async/DownloadAsyncTask.java b/app/src/main/java/com/aaronjwood/portauthority/async/DownloadAsyncTask.java index 1b2da869..305d1ad2 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/async/DownloadAsyncTask.java +++ b/app/src/main/java/com/aaronjwood/portauthority/async/DownloadAsyncTask.java @@ -13,15 +13,26 @@ import java.io.IOException; import java.io.InputStreamReader; import java.lang.ref.WeakReference; -import java.net.URL; -import java.nio.charset.StandardCharsets; import java.util.zip.GZIPInputStream; -import javax.net.ssl.HttpsURLConnection; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; -public abstract class DownloadAsyncTask extends AsyncTask { +class DownloadProgress { + public String message; + public int progress; + @Override + public String toString() { + return message; + } +} + +public abstract class DownloadAsyncTask extends AsyncTask { private ProgressDialog dialog; + private String failedDbInsert; protected Database db; protected WeakReference delegate; @@ -39,8 +50,13 @@ protected void onPreExecute() { } Context ctx = (Context) activity; + this.failedDbInsert = ctx.getResources().getString(R.string.failedDbInsert); + dialog = new ProgressDialog(ctx, R.style.DialogTheme); + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setMessage(ctx.getResources().getString(R.string.downloadingData)); + dialog.setIndeterminate(false); + dialog.setMax(100); dialog.setCanceledOnTouchOutside(false); dialog.setOnCancelListener(dialogInterface -> { dialogInterface.cancel(); @@ -57,41 +73,61 @@ protected void onPreExecute() { */ final void doInBackground(String service, Parser parser) { BufferedReader in = null; - HttpsURLConnection connection = null; db.beginTransaction(); + DownloadProgress downProg = new DownloadProgress(); + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(service) + .addHeader("Accept-Encoding", "gzip") + .build(); try { - URL url = new URL(service); - connection = (HttpsURLConnection) url.openConnection(); - connection.setRequestProperty("Accept-Encoding", "gzip"); - connection.connect(); - if (connection.getResponseCode() != HttpsURLConnection.HTTP_OK) { - publishProgress(connection.getResponseCode() + " " + connection.getResponseMessage()); - - return; - } - - in = new BufferedReader(new InputStreamReader(new GZIPInputStream(connection.getInputStream()), "UTF-8")); - String line; - - while ((line = in.readLine()) != null) { - if (isCancelled()) { + try (Response response = client.newCall(request).execute()) { + ResponseBody body = response.body(); + if (body == null) { + downProg.message = String.valueOf(response.code()); return; } - String[] data = parser.parseLine(line); - if (data == null) { - continue; + if (!response.isSuccessful()) { + downProg.message = body.string(); + publishProgress(downProg); + return; } - if (parser.saveLine(db, data) == -1) { - publishProgress("Failed to insert data into the database. Please run this operation again"); - - return; + in = new BufferedReader(new InputStreamReader(new GZIPInputStream(body.byteStream()), "UTF-8")); + String line; + long total = 0; + while ((line = in.readLine()) != null) { + if (isCancelled()) { + return; + } + + // Lean on the fact that we're working with UTF-8 here. + // Also, make a rough estimation of how much we need to reduce this to account for the compressed data we've received. + total += line.length() / 3; + downProg.progress = (int) (total * 100 / body.contentLength()); + publishProgress(downProg); + String[] data = parser.parseLine(line); + if (data == null) { + continue; + } + + if (parser.exportLine(db, data) == -1) { + MainAsyncResponse activity = delegate.get(); + if (activity != null) { + dialog.dismiss(); + } + downProg.message = this.failedDbInsert; + publishProgress(downProg); + return; + } } + + db.setTransactionSuccessful(); } - db.setTransactionSuccessful(); } catch (Exception e) { - publishProgress(e.toString()); + downProg.message = e.toString(); + publishProgress(downProg); } finally { db.endTransaction(); try { @@ -100,24 +136,19 @@ final void doInBackground(String service, Parser parser) { } } catch (IOException ignored) { } - - if (connection != null) { - connection.disconnect(); - } } } - /** - * Handles errors. - * - * @param progress - */ @Override - protected void onProgressUpdate(String... progress) { + protected void onProgressUpdate(DownloadProgress... progress) { + DownloadProgress currProg = progress[0]; MainAsyncResponse activity = delegate.get(); - if (activity != null) { - activity.processFinish(new Exception(progress[0])); + if (currProg.message != null && activity != null) { + activity.processFinish(new Exception(currProg.message)); + return; } + + dialog.setProgress(currProg.progress); } /** diff --git a/app/src/main/java/com/aaronjwood/portauthority/async/DownloadOuisAsyncTask.java b/app/src/main/java/com/aaronjwood/portauthority/async/DownloadOuisAsyncTask.java index 96d091bb..11341067 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/async/DownloadOuisAsyncTask.java +++ b/app/src/main/java/com/aaronjwood/portauthority/async/DownloadOuisAsyncTask.java @@ -9,7 +9,9 @@ public class DownloadOuisAsyncTask extends DownloadAsyncTask { - private static final String SERVICE = "https://code.wireshark.org/review/gitweb?p=wireshark.git;a=blob_plain;f=manuf"; + // The official source on gitlab.com doesn't provide a content length header which ruins our ability to report progress! + // Use the mirror from github.com instead. + private static final String SERVICE = "https://raw.githubusercontent.com/wireshark/wireshark/master/manuf"; /** * Creates a new asynchronous task to handle downloading OUI data. @@ -24,14 +26,6 @@ public DownloadOuisAsyncTask(Database database, Parser parser, MainAsyncResponse this.parser = parser; } - /** - * Sets up and displays the dialog. - */ - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - /** * Downloads new OUI data. * diff --git a/app/src/main/java/com/aaronjwood/portauthority/async/DownloadPortDataAsyncTask.java b/app/src/main/java/com/aaronjwood/portauthority/async/DownloadPortDataAsyncTask.java index afe1fc36..1e0845c1 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/async/DownloadPortDataAsyncTask.java +++ b/app/src/main/java/com/aaronjwood/portauthority/async/DownloadPortDataAsyncTask.java @@ -23,14 +23,6 @@ public DownloadPortDataAsyncTask(Database database, Parser parser, MainAsyncResp this.parser = parser; } - /** - * Displays the progress dialog. - */ - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - /** * Downloads new port data. * diff --git a/app/src/main/java/com/aaronjwood/portauthority/async/ScanHostsAsyncTask.java b/app/src/main/java/com/aaronjwood/portauthority/async/ScanHostsAsyncTask.java index 499a394d..a99a78f7 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/async/ScanHostsAsyncTask.java +++ b/app/src/main/java/com/aaronjwood/portauthority/async/ScanHostsAsyncTask.java @@ -1,9 +1,11 @@ package com.aaronjwood.portauthority.async; +import android.content.Context; import android.os.AsyncTask; import android.os.Build; import android.util.Pair; +import com.aaronjwood.portauthority.R; import com.aaronjwood.portauthority.db.Database; import com.aaronjwood.portauthority.network.Host; import com.aaronjwood.portauthority.response.MainAsyncResponse; @@ -29,7 +31,7 @@ public class ScanHostsAsyncTask extends AsyncTask { private final WeakReference delegate; - private Database db; + private final Database db; private static final String ARP_TABLE = "/proc/net/arp"; private static final String IP_CMD = "ip neighbor"; private static final String NEIGHBOR_INCOMPLETE = "INCOMPLETE"; @@ -59,6 +61,7 @@ protected Void doInBackground(Integer... params) { int cidr = params[1]; int timeout = params[2]; MainAsyncResponse activity = delegate.get(); + Context ctx = (Context) activity; // Android 10+ doesn't let us access the ARP table. // Do an early check to see if we can get what we need from the system. @@ -68,26 +71,26 @@ protected Void doInBackground(Integer... params) { Process ipProc = Runtime.getRuntime().exec(IP_CMD); ipProc.waitFor(); if (ipProc.exitValue() != 0) { - activity.processFinish(new IOException("Unable to access ARP entries")); + activity.processFinish(new IOException(ctx.getResources().getString(R.string.errAccessArp))); activity.processFinish(true); return null; } } catch (IOException | InterruptedException e) { - activity.processFinish(new IOException("Unable to parse ARP entries")); + activity.processFinish(new IOException(ctx.getResources().getString(R.string.errParseArp))); activity.processFinish(true); } } else { File file = new File(ARP_TABLE); if (!file.exists()) { - activity.processFinish(new FileNotFoundException("Unable to find ARP table")); + activity.processFinish(new FileNotFoundException(ctx.getResources().getString(R.string.errFindArp))); activity.processFinish(true); return null; } if (!file.canRead()) { - activity.processFinish(new IOException("Unable to read ARP table")); + activity.processFinish(new IOException(ctx.getResources().getString(R.string.errReadArp))); activity.processFinish(true); } } @@ -139,13 +142,14 @@ protected final void onProgressUpdate(Void... params) { ExecutorService executor = Executors.newCachedThreadPool(); final AtomicInteger numHosts = new AtomicInteger(0); List> pairs = new ArrayList<>(); + Context ctx = (Context) activity; try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Process ipProc = Runtime.getRuntime().exec(IP_CMD); ipProc.waitFor(); if (ipProc.exitValue() != 0) { - throw new Exception("Unable to access ARP entries"); + throw new Exception(ctx.getResources().getString(R.string.errAccessArp)); } reader = new BufferedReader(new InputStreamReader(ipProc.getInputStream(), "UTF-8")); @@ -199,7 +203,15 @@ protected final void onProgressUpdate(Void... params) { try { host = new Host(ip, macAddress, db); } catch (IOException e) { - host = new Host(ip, macAddress); + try { + host = new Host(ip, macAddress); + } catch (UnknownHostException ex) { + if (activity != null) { + activity.processFinish(e); + } + + return; + } } MainAsyncResponse activity1 = delegate.get(); diff --git a/app/src/main/java/com/aaronjwood/portauthority/async/WanIpAsyncTask.java b/app/src/main/java/com/aaronjwood/portauthority/async/WanIpAsyncTask.java index fc59960e..e4b2d0ae 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/async/WanIpAsyncTask.java +++ b/app/src/main/java/com/aaronjwood/portauthority/async/WanIpAsyncTask.java @@ -1,8 +1,11 @@ package com.aaronjwood.portauthority.async; import android.annotation.SuppressLint; +import android.content.Context; import android.os.AsyncTask; +import com.aaronjwood.portauthority.R; +import com.aaronjwood.portauthority.response.HostAsyncResponse; import com.aaronjwood.portauthority.response.MainAsyncResponse; import java.io.IOException; @@ -11,6 +14,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import okhttp3.ResponseBody; public class WanIpAsyncTask extends AsyncTask { @@ -36,18 +40,20 @@ public WanIpAsyncTask(MainAsyncResponse delegate) { @Override @SuppressLint("NewApi") protected String doInBackground(Void... params) { - String error = "Couldn't get your external IP"; + MainAsyncResponse activity = delegate.get(); + Context ctx = (Context) activity; OkHttpClient httpClient = new OkHttpClient(); Request request = new Request.Builder().url(EXTERNAL_IP_SERVICE).build(); try (Response response = httpClient.newCall(request).execute()) { - if (!response.isSuccessful()) { - return error; + ResponseBody body = response.body(); + if (!response.isSuccessful() || body == null) { + return ctx.getResources().getString(R.string.errExternIp); } - return response.body().string().trim(); + return body.string().trim(); } catch (IOException e) { - return error; + return ctx.getResources().getString(R.string.errExternIp); } } diff --git a/app/src/main/java/com/aaronjwood/portauthority/db/Database.java b/app/src/main/java/com/aaronjwood/portauthority/db/Database.java index bc62c2ba..f9e56e3c 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/db/Database.java +++ b/app/src/main/java/com/aaronjwood/portauthority/db/Database.java @@ -22,7 +22,7 @@ public class Database extends SQLiteOpenHelper { private static final String CREATE_MAC_INDEX = "CREATE INDEX IF NOT EXISTS idx_ouis_mac ON " + OUI_TABLE + " (" + MAC_FIELD + ");"; private static Database singleton; - private SQLiteDatabase db; + private final SQLiteDatabase db; /** * Returns the single instance of this class or creates one if it doesn't already exist. @@ -51,31 +51,25 @@ private Database(Context context) { /** * Starts a transaction that allows for multiple readers and one writer. * - * @return */ - public Database beginTransaction() { + public void beginTransaction() { db.beginTransactionNonExclusive(); - return this; } /** * Finishes the transaction. * - * @return */ - public Database endTransaction() { + public void endTransaction() { db.endTransaction(); - return this; } /** * Marks the transaction as successful and commits the transaction. * - * @return */ - public Database setTransactionSuccessful() { + public void setTransactionSuccessful() { db.setTransactionSuccessful(); - return this; } /** @@ -142,23 +136,19 @@ public long insertPort(String port, String description) { /** * Wipes out all of the OUIs that are currently in the database. * - * @return */ - public Database clearOuis() { + public void clearOuis() { db.execSQL("DELETE FROM " + OUI_TABLE); db.execSQL("VACUUM"); - return this; } /** * Wipes out all of the ports that are currently in the database. * - * @return */ - public Database clearPorts() { + public void clearPorts() { db.execSQL("DELETE FROM " + PORT_TABLE); db.execSQL("VACUUM"); - return this; } /** @@ -170,14 +160,13 @@ public Database clearPorts() { public String selectVendor(String mac) { Cursor cursor = db.rawQuery("SELECT " + VENDOR_FIELD + " FROM " + OUI_TABLE + " WHERE " + MAC_FIELD + " = ?", new String[]{mac}); String vendor; - if (cursor.moveToFirst()) { - vendor = cursor.getString(cursor.getColumnIndex("vendor")); - } else { - vendor = "Vendor not in database"; + if (!cursor.moveToFirst()) { + cursor.close(); + return null; } + vendor = cursor.getString(cursor.getColumnIndex("vendor")); cursor.close(); - return vendor; } diff --git a/app/src/main/java/com/aaronjwood/portauthority/listener/ScanPortsListener.java b/app/src/main/java/com/aaronjwood/portauthority/listener/ScanPortsListener.java index fd122a7c..fc3e261c 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/listener/ScanPortsListener.java +++ b/app/src/main/java/com/aaronjwood/portauthority/listener/ScanPortsListener.java @@ -7,8 +7,8 @@ public class ScanPortsListener implements View.OnClickListener { - private List ports; - private ArrayAdapter adapter; + private final List ports; + private final ArrayAdapter adapter; /** * New click listener for scanning ports diff --git a/app/src/main/java/com/aaronjwood/portauthority/network/Host.java b/app/src/main/java/com/aaronjwood/portauthority/network/Host.java index fdd59922..cd078411 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/network/Host.java +++ b/app/src/main/java/com/aaronjwood/portauthority/network/Host.java @@ -1,6 +1,7 @@ package com.aaronjwood.portauthority.network; import android.database.sqlite.SQLiteException; +import android.os.AsyncTask; import com.aaronjwood.portauthority.async.ScanPortsAsyncTask; import com.aaronjwood.portauthority.async.WolAsyncTask; @@ -9,12 +10,15 @@ import java.io.IOException; import java.io.Serializable; +import java.net.InetAddress; +import java.net.UnknownHostException; public class Host implements Serializable { private String hostname; - private String ip; - private String mac; + private final String ip; + private final byte[] address; + private final String mac; private String vendor; public Host(String ip, String mac, Database db) throws IOException { @@ -28,8 +32,9 @@ public Host(String ip, String mac, Database db) throws IOException { * @param ip * @param mac */ - public Host(String ip, String mac) { + public Host(String ip, String mac) throws UnknownHostException { this.ip = ip; + this.address = InetAddress.getByName(ip).getAddress(); this.mac = mac; } @@ -46,18 +51,15 @@ public String getHostname() { * Sets this host's hostname to the given value * * @param hostname Hostname for this host - * @return */ - public Host setHostname(String hostname) { + public void setHostname(String hostname) { this.hostname = hostname; - return this; } - private Host setVendor(Database db) throws IOException { + private void setVendor(Database db) { vendor = findMacVendor(mac, db); - return this; } /** @@ -78,6 +80,15 @@ public String getIp() { return ip; } + /** + * Returns this host's address in byte representation. + * + * @return + */ + public byte[] getAddress() { + return this.address; + } + /** * Returns this host's MAC address * @@ -88,7 +99,7 @@ public String getMac() { } public void wakeOnLan() { - new WolAsyncTask().execute(mac, ip); + new WolAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mac, ip); } /** @@ -101,7 +112,7 @@ public void wakeOnLan() { * @param delegate Delegate to be called when the port scan has finished */ public static void scanPorts(String ip, int startPort, int stopPort, int timeout, HostAsyncResponse delegate) { - new ScanPortsAsyncTask(delegate).execute(ip, startPort, stopPort, timeout); + new ScanPortsAsyncTask(delegate).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, ip, startPort, stopPort, timeout); } /** @@ -113,9 +124,23 @@ public static void scanPorts(String ip, int startPort, int stopPort, int timeout * @throws IOException * @throws SQLiteException */ - public static String findMacVendor(String mac, Database db) throws IOException, SQLiteException { + public static String findMacVendor(String mac, Database db) throws SQLiteException { String prefix = mac.substring(0, 8); - return db.selectVendor(prefix); + String vendor = db.selectVendor(prefix); + if (vendor != null) { + return vendor; + } + + String notInDb = "Vendor not in database"; + char identifier = mac.charAt(1); + if ("26ae".indexOf(identifier) != -1) { + return notInDb + " (private address)"; + } + + if ("13579bdf".indexOf(identifier) != -1) { + return notInDb + " (multicast address)"; + } + + return notInDb; } - } diff --git a/app/src/main/java/com/aaronjwood/portauthority/network/Wireless.java b/app/src/main/java/com/aaronjwood/portauthority/network/Wireless.java index ef4850df..677f93eb 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/network/Wireless.java +++ b/app/src/main/java/com/aaronjwood/portauthority/network/Wireless.java @@ -6,6 +6,7 @@ import android.net.NetworkInfo; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; +import android.os.AsyncTask; import com.aaronjwood.portauthority.async.WanIpAsyncTask; import com.aaronjwood.portauthority.response.MainAsyncResponse; @@ -22,7 +23,7 @@ public class Wireless { - private Context context; + private final Context context; public static class NoWifiManagerException extends Exception { } @@ -150,10 +151,6 @@ public T getInternalWifiIpAddress(Class type) throws UnknownHostException */ public int getInternalWifiSubnet() throws NoWifiManagerException { WifiManager wifiManager = getWifiManager(); - if (wifiManager == null) { - return 0; - } - DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); if (dhcpInfo == null) { return 0; @@ -193,7 +190,7 @@ public int getInternalWifiSubnet() throws NoWifiManagerException { * @return Number of hosts as an integer. */ public int getNumberOfHostsInWifiSubnet() throws NoWifiManagerException { - Double subnet = (double) getInternalWifiSubnet(); + double subnet = getInternalWifiSubnet(); double hosts; double bitsLeft = 32.0d - subnet; hosts = Math.pow(2.0d, bitsLeft) - 2.0d; @@ -231,7 +228,7 @@ public static String getInternalMobileIpAddress() { * @param delegate Called when the external IP address has been fetched */ public void getExternalIpAddress(MainAsyncResponse delegate) { - new WanIpAsyncTask(delegate).execute(); + new WanIpAsyncTask(delegate).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } /** diff --git a/app/src/main/java/com/aaronjwood/portauthority/parser/OuiParser.java b/app/src/main/java/com/aaronjwood/portauthority/parser/OuiParser.java index d88cda07..e6f673a9 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/parser/OuiParser.java +++ b/app/src/main/java/com/aaronjwood/portauthority/parser/OuiParser.java @@ -26,18 +26,17 @@ public String[] parseLine(String line) { } return new String[]{mac, vendor}; - } /** - * Saves the parsed line of OUI data to the database. + * Exports the parsed line of OUI data to the database. * * @param db * @param line * @return */ @Override - public long saveLine(Database db, String[] line) { + public long exportLine(Database db, String[] line) { return db.insertOui(line[0], line[1]); } diff --git a/app/src/main/java/com/aaronjwood/portauthority/parser/Parser.java b/app/src/main/java/com/aaronjwood/portauthority/parser/Parser.java index ca8da18b..ce82c318 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/parser/Parser.java +++ b/app/src/main/java/com/aaronjwood/portauthority/parser/Parser.java @@ -13,12 +13,12 @@ public interface Parser { String[] parseLine(String line); /** - * Saves a parsed line of data to the database. + * Exports a parsed line of data to the database. * * @param db * @param data * @return */ - long saveLine(Database db, String[] data); + long exportLine(Database db, String[] data); } diff --git a/app/src/main/java/com/aaronjwood/portauthority/parser/PortParser.java b/app/src/main/java/com/aaronjwood/portauthority/parser/PortParser.java index 4bd7b6a7..674d7600 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/parser/PortParser.java +++ b/app/src/main/java/com/aaronjwood/portauthority/parser/PortParser.java @@ -29,14 +29,14 @@ public String[] parseLine(String line) { } /** - * Saves the parsed line to the database. + * Exports the parsed line to the database. * * @param db * @param data * @return */ @Override - public long saveLine(Database db, String[] data) { + public long exportLine(Database db, String[] data) { return db.insertPort(data[0], data[1]); } } diff --git a/app/src/main/java/com/aaronjwood/portauthority/runnable/ScanHostsRunnable.java b/app/src/main/java/com/aaronjwood/portauthority/runnable/ScanHostsRunnable.java index 6805dccc..20731905 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/runnable/ScanHostsRunnable.java +++ b/app/src/main/java/com/aaronjwood/portauthority/runnable/ScanHostsRunnable.java @@ -10,9 +10,9 @@ import java.net.Socket; public class ScanHostsRunnable implements Runnable { - private int start; - private int stop; - private int timeout; + private final int start; + private final int stop; + private final int timeout; private final WeakReference delegate; /** @@ -36,8 +36,7 @@ public ScanHostsRunnable(int start, int stop, int timeout, WeakReference delegate; /** diff --git a/app/src/main/java/com/aaronjwood/portauthority/utils/UserPreference.java b/app/src/main/java/com/aaronjwood/portauthority/utils/UserPreference.java index b2fbca77..11bb890a 100644 --- a/app/src/main/java/com/aaronjwood/portauthority/utils/UserPreference.java +++ b/app/src/main/java/com/aaronjwood/portauthority/utils/UserPreference.java @@ -3,9 +3,9 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import android.support.annotation.IntRange; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; /** * Utility Class for getting certain user preferences diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 58463495..747cd512 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -1,4 +1,4 @@ - @@ -305,4 +305,4 @@ android:entries="@array/lowerDrawer" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c83b7c13..6983e0c5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,4 +1,4 @@ - @@ -227,4 +227,4 @@ - + diff --git a/app/src/main/res/layout/checkbox.xml b/app/src/main/res/layout/checkbox.xml index f7019caa..e4a2c9dd 100644 --- a/app/src/main/res/layout/checkbox.xml +++ b/app/src/main/res/layout/checkbox.xml @@ -1,6 +1,6 @@ - + android:paddingBottom="5dp" + android:inputType="text" /> + android:paddingBottom="5dp" + android:inputType="text" />