diff --git a/images/home.png b/images/home.png new file mode 100644 index 0000000..ff29557 Binary files /dev/null and b/images/home.png differ diff --git a/images/sms.png b/images/sms.png new file mode 100644 index 0000000..5b87743 Binary files /dev/null and b/images/sms.png differ diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/src/.idea/.gitignore b/src/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/src/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/src/.idea/.name b/src/.idea/.name new file mode 100644 index 0000000..503ea25 --- /dev/null +++ b/src/.idea/.name @@ -0,0 +1 @@ +Locate my device \ No newline at end of file diff --git a/src/.idea/compiler.xml b/src/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/src/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/.idea/deploymentTargetDropDown.xml b/src/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..f0276b9 --- /dev/null +++ b/src/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/.idea/gradle.xml b/src/.idea/gradle.xml new file mode 100644 index 0000000..a2d7c21 --- /dev/null +++ b/src/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/src/.idea/misc.xml b/src/.idea/misc.xml new file mode 100644 index 0000000..b286d63 --- /dev/null +++ b/src/.idea/misc.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/.gitignore b/src/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/src/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/src/app/build.gradle b/src/app/build.gradle new file mode 100644 index 0000000..9fc1f12 --- /dev/null +++ b/src/app/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "com.xfarrow.locatemydevice" + minSdk 28 + targetSdk 32 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.google.android.material:material:1.6.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + +} \ No newline at end of file diff --git a/src/app/proguard-rules.pro b/src/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/src/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 \ No newline at end of file diff --git a/src/app/release/app-release.apk b/src/app/release/app-release.apk new file mode 100644 index 0000000..2af74d0 Binary files /dev/null and b/src/app/release/app-release.apk differ diff --git a/src/app/release/output-metadata.json b/src/app/release/output-metadata.json new file mode 100644 index 0000000..6d99d3f --- /dev/null +++ b/src/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.xfarrow.locatemydevice", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File" +} \ No newline at end of file diff --git a/src/app/src/androidTest/java/com/xfarrow/locatemydevice/ExampleInstrumentedTest.java b/src/app/src/androidTest/java/com/xfarrow/locatemydevice/ExampleInstrumentedTest.java new file mode 100644 index 0000000..966f4e1 --- /dev/null +++ b/src/app/src/androidTest/java/com/xfarrow/locatemydevice/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.xfarrow.locatemydevice; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.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.getInstrumentation().getTargetContext(); + assertEquals("com.xfarrow.locatemydevice", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/src/app/src/main/AndroidManifest.xml b/src/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..555e770 --- /dev/null +++ b/src/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/java/com/xfarrow/locatemydevice/CipherUtils.java b/src/app/src/main/java/com/xfarrow/locatemydevice/CipherUtils.java new file mode 100644 index 0000000..8f8857b --- /dev/null +++ b/src/app/src/main/java/com/xfarrow/locatemydevice/CipherUtils.java @@ -0,0 +1,26 @@ +package com.xfarrow.locatemydevice; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class CipherUtils { + // Return SHA-256 hash if possible, plaintext otherwise + public static String get256Sha(String string) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(string.getBytes(StandardCharsets.UTF_8)); + BigInteger number = new BigInteger(1, hash); + StringBuilder hexString = new StringBuilder(number.toString(16)); + while (hexString.length() < 64) { + hexString.insert(0, '0'); + } + return hexString.toString(); + } catch(NoSuchAlgorithmException ex){ + return string; + } + + } + +} diff --git a/src/app/src/main/java/com/xfarrow/locatemydevice/MainActivity.java b/src/app/src/main/java/com/xfarrow/locatemydevice/MainActivity.java new file mode 100644 index 0000000..a87f36f --- /dev/null +++ b/src/app/src/main/java/com/xfarrow/locatemydevice/MainActivity.java @@ -0,0 +1,66 @@ +package com.xfarrow.locatemydevice; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; + +import android.Manifest; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; + +public class MainActivity extends AppCompatActivity { + private final int MY_PERMISSIONS_REQUEST_SMS_RECEIVE = 10; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + requirePermissions(); + } + + private void requirePermissions(){ + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.RECEIVE_SMS}, + MY_PERMISSIONS_REQUEST_SMS_RECEIVE); // Understand what this is + + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.SEND_SMS}, + 200); // Understand what this is + + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + 1); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == MY_PERMISSIONS_REQUEST_SMS_RECEIVE) { + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.settings_button) { + Intent myIntent = new Intent(MainActivity.this, SettingsActivity.class); + MainActivity.this.startActivity(myIntent); + } + return super.onOptionsItemSelected(item); + } +} \ No newline at end of file diff --git a/src/app/src/main/java/com/xfarrow/locatemydevice/Settings.java b/src/app/src/main/java/com/xfarrow/locatemydevice/Settings.java new file mode 100644 index 0000000..55069dd --- /dev/null +++ b/src/app/src/main/java/com/xfarrow/locatemydevice/Settings.java @@ -0,0 +1,35 @@ +package com.xfarrow.locatemydevice; + +import android.content.Context; +import android.content.SharedPreferences; + +public class Settings { + public static final int PASSWORD = 0; + public static final int SMS_COMMAND = 1; + + private final SharedPreferences sharedPreferences; + + public Settings(Context context){ + sharedPreferences = context.getSharedPreferences("locatemydevice.settings", Context.MODE_PRIVATE); + } + + public void set(int setting, String value){ + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(String.valueOf(setting), value); + editor.apply(); + } + + public String get(int setting){ + return sharedPreferences.getString(String.valueOf(setting), defaultValues(setting)); + } + + public String defaultValues(int setting){ + switch (setting){ + case PASSWORD: + return CipherUtils.get256Sha("0000"); + case SMS_COMMAND: + return "LMD"; + } + return null; + } +} diff --git a/src/app/src/main/java/com/xfarrow/locatemydevice/SettingsActivity.java b/src/app/src/main/java/com/xfarrow/locatemydevice/SettingsActivity.java new file mode 100644 index 0000000..caf99a5 --- /dev/null +++ b/src/app/src/main/java/com/xfarrow/locatemydevice/SettingsActivity.java @@ -0,0 +1,109 @@ +package com.xfarrow.locatemydevice; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +public class SettingsActivity extends AppCompatActivity { + + private Button buttonEnterPin; + private EditText editTextLmdCommand; + private Settings settings; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.settings); + settings = new Settings(this); + setViews(); + setValues(); + setListeners(); + } + + private void setViews(){ + buttonEnterPin = findViewById(R.id.buttonEnterPassword); + editTextLmdCommand = findViewById(R.id.editTextLmdCommand); + } + + private void setValues(){ + editTextLmdCommand.setText(settings.get(Settings.SMS_COMMAND)); + } + + private void setListeners(){ + buttonEnterPin.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + AlertDialog.Builder alert = new AlertDialog.Builder(SettingsActivity.this); + alert.setTitle("Password"); + alert.setMessage("Enter Password"); + EditText input = new EditText(SettingsActivity.this); + input.setTransformationMethod(new PasswordTransformationMethod()); + alert.setView(input); + + alert.setPositiveButton("OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + String text = input.getText().toString(); + if (!text.isEmpty()) { + settings.set(Settings.PASSWORD, CipherUtils.get256Sha(text)); + } + else{ + Toast.makeText(SettingsActivity.this, "Cannot use a blank password. Aborted!", Toast.LENGTH_LONG).show(); + } + } + }); + + alert.setNeutralButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.cancel(); + } + }); + + final AlertDialog dialog = alert.create(); + dialog.show(); + + // Disable button "OK" if the PIN contains a space or it's empty. + input.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled( + !charSequence.toString().equals("") && !charSequence.toString().contains(" ") + ); + } + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + @Override + public void afterTextChanged(Editable editable) {} + }); + } + }); + + editTextLmdCommand.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (s.toString().isEmpty()) { + Toast.makeText(SettingsActivity.this, "Empty SMS command not allowed, reverted to default (LMD)", Toast.LENGTH_LONG).show(); + settings.set(Settings.SMS_COMMAND, settings.defaultValues(Settings.SMS_COMMAND)); + } else { + settings.set(Settings.SMS_COMMAND, s.toString()); + } + } + }); + } +} diff --git a/src/app/src/main/java/com/xfarrow/locatemydevice/SmsHandler.java b/src/app/src/main/java/com/xfarrow/locatemydevice/SmsHandler.java new file mode 100644 index 0000000..68c32ed --- /dev/null +++ b/src/app/src/main/java/com/xfarrow/locatemydevice/SmsHandler.java @@ -0,0 +1,113 @@ +package com.xfarrow.locatemydevice; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Build; +import android.os.Bundle; +import android.telephony.SmsManager; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.app.ActivityCompat; + +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SmsHandler { + /* + * Messages: + * finddevice 1234 locate + * [command] [password] [option] + * + */ + @RequiresApi(api = Build.VERSION_CODES.R) + public void handleSms(String message, String sender, Context context) { + Settings settings = new Settings(context); + String password = settings.get(Settings.PASSWORD); + String command = settings.get(Settings.SMS_COMMAND); + String providedOption = ""; + String providedPassword = ""; + + String regexToMatch = "^" + + command + + "\\s" + + "[^\\s]*" + + "\\s" + + Utils.LOCATE_OPTION; + Pattern pattern = Pattern.compile(regexToMatch); + Matcher matcher = pattern.matcher(message); + if (!matcher.find()) { + return; + } + + String[] splitMessage = message.split(" "); + providedPassword = splitMessage[1]; + providedOption = splitMessage[2]; + + if (!CipherUtils.get256Sha(providedPassword).equals(password)) { + return; + } + + SmsManager smsManager = SmsManager.getDefault(); + + // locate + if (providedOption.equals(Settings.LOCATE_OPTION)) { + + LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + + // GPS is off + if (!locationManager.isLocationEnabled()) { + smsManager.sendTextMessage(sender, null, "GPS is off ", null, null); + return; + } + + // GPS permission not granted + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + smsManager.sendTextMessage(sender, null, + "GPS permission is not granted. " + + "Unable to serve request.",null, null); + return; + } + + // API 30 and above + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + locationManager.getCurrentLocation(LocationManager.GPS_PROVIDER, null, context.getMainExecutor(), new Consumer() { + @Override + public void accept(Location location) { + double latitude = location.getLatitude(); + double longitude = location.getLongitude(); + smsManager.sendTextMessage(sender, null, + "GPS coordinates are: " + + "\nLatitude: " + latitude + + "\nLongitude: " + longitude + "\n" + + Utils.buildOSMLink(latitude, longitude), null, null); + } + }); + } + + // Legacy (API < 29) + else{ + Criteria locationCriteria = new Criteria(); + locationCriteria.setAccuracy(Criteria.ACCURACY_FINE); + locationManager.requestSingleUpdate(locationCriteria, new LocationListener() { + @Override + public void onLocationChanged(@NonNull Location location) { + double latitude = location.getLatitude(); + double longitude = location.getLongitude(); + smsManager.sendTextMessage(sender, null, + "GPS coordinates are:" + + "\nLatitude: " + latitude + + "\nLongitude: " + longitude + "\n" + + Utils.buildOSMLink(latitude, longitude), null, null); + } + }, null); + } + } + } +} diff --git a/src/app/src/main/java/com/xfarrow/locatemydevice/SmsReceiver.java b/src/app/src/main/java/com/xfarrow/locatemydevice/SmsReceiver.java new file mode 100644 index 0000000..b67236e --- /dev/null +++ b/src/app/src/main/java/com/xfarrow/locatemydevice/SmsReceiver.java @@ -0,0 +1,47 @@ +package com.xfarrow.locatemydevice; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.telephony.SmsMessage; +import android.util.Log; +import android.widget.Toast; + +public class SmsReceiver extends BroadcastReceiver { + + // This method gets fired as soon as it receives an SMS + @Override + public void onReceive(Context context, Intent intent) { + + if (intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) { + Bundle bundle = intent.getExtras(); + if (bundle != null) { + // get sms objects + Object[] pdus = (Object[]) bundle.get("pdus"); + if (pdus.length == 0) { + return; + } + String format = bundle.getString("format"); + // large message might be broken into many + SmsMessage[] messages = new SmsMessage[pdus.length]; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pdus.length; i++) { + messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i], format); + sb.append(messages[i].getMessageBody()); + } + // getOriginatingAddress() will always be the same in messages[X] for 0<=X + + diff --git a/src/app/src/main/res/drawable-hdpi/settings.png b/src/app/src/main/res/drawable-hdpi/settings.png new file mode 100644 index 0000000..8bb1b83 Binary files /dev/null and b/src/app/src/main/res/drawable-hdpi/settings.png differ diff --git a/src/app/src/main/res/drawable-mdpi/settings.png b/src/app/src/main/res/drawable-mdpi/settings.png new file mode 100644 index 0000000..fe710f7 Binary files /dev/null and b/src/app/src/main/res/drawable-mdpi/settings.png differ diff --git a/src/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/src/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable-xhdpi/settings.png b/src/app/src/main/res/drawable-xhdpi/settings.png new file mode 100644 index 0000000..8b683c6 Binary files /dev/null and b/src/app/src/main/res/drawable-xhdpi/settings.png differ diff --git a/src/app/src/main/res/drawable-xxhdpi/settings.png b/src/app/src/main/res/drawable-xxhdpi/settings.png new file mode 100644 index 0000000..c3a05ec Binary files /dev/null and b/src/app/src/main/res/drawable-xxhdpi/settings.png differ diff --git a/src/app/src/main/res/drawable/ic_launcher_background.xml b/src/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/src/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/src/main/res/drawable/logo.png b/src/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000..73f508b Binary files /dev/null and b/src/app/src/main/res/drawable/logo.png differ diff --git a/src/app/src/main/res/layout/activity_main.xml b/src/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..be7a530 --- /dev/null +++ b/src/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/layout/settings.xml b/src/app/src/main/res/layout/settings.xml new file mode 100644 index 0000000..51d0357 --- /dev/null +++ b/src/app/src/main/res/layout/settings.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + +