diff --git a/OpenPods/.idea/dictionaries/Electric.xml b/OpenPods/.idea/dictionaries/Electric.xml
new file mode 100644
index 0000000..4da9183
--- /dev/null
+++ b/OpenPods/.idea/dictionaries/Electric.xml
@@ -0,0 +1,8 @@
+
+
+
+ airpod
+ airpods
+
+
+
\ No newline at end of file
diff --git a/OpenPods/.idea/misc.xml b/OpenPods/.idea/misc.xml
index af0bbdd..703e5d4 100644
--- a/OpenPods/.idea/misc.xml
+++ b/OpenPods/.idea/misc.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/OpenPods/.idea/render.experimental.xml b/OpenPods/.idea/render.experimental.xml
new file mode 100644
index 0000000..8ec256a
--- /dev/null
+++ b/OpenPods/.idea/render.experimental.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OpenPods/app/build.gradle b/OpenPods/app/build.gradle
index f201df8..915fc1a 100644
--- a/OpenPods/app/build.gradle
+++ b/OpenPods/app/build.gradle
@@ -1,15 +1,19 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 28
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
defaultConfig {
applicationId "com.dosse.airpods"
minSdkVersion 23
- targetSdkVersion 23
+ targetSdkVersion 29
versionCode 13
versionName '1.3'
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
+
buildTypes {
release {
minifyEnabled false
@@ -21,9 +25,14 @@ android {
lintOptions {
abortOnError false
}
+ compileOptions {
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ }
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
- implementation 'com.android.support:appcompat-v7:28.0.0'
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation "androidx.preference:preference:1.1.1"
}
diff --git a/OpenPods/app/src/main/AndroidManifest.xml b/OpenPods/app/src/main/AndroidManifest.xml
index 21a6422..7b3f096 100644
--- a/OpenPods/app/src/main/AndroidManifest.xml
+++ b/OpenPods/app/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
@@ -16,14 +17,28 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:supportsRtl="true"
- android:theme="@style/AppTheme">
-
-
+ android:supportsRtl="false"
+ android:theme="@style/AppTheme"
+ tools:ignore="AllowBackup">
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:exported="true"
+ tools:ignore="ExportedService" />
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/OpenPods/app/src/main/java/com/dosse/airpods/AboutActivity.java b/OpenPods/app/src/main/java/com/dosse/airpods/AboutActivity.java
index e5293a2..80e1ab7 100644
--- a/OpenPods/app/src/main/java/com/dosse/airpods/AboutActivity.java
+++ b/OpenPods/app/src/main/java/com/dosse/airpods/AboutActivity.java
@@ -2,21 +2,24 @@ package com.dosse.airpods;
import android.content.Intent;
import android.net.Uri;
-import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
-import android.view.View;
import android.webkit.WebView;
-import android.widget.Button;
import android.widget.Toast;
+import androidx.appcompat.app.AppCompatActivity;
+
public class AboutActivity extends AppCompatActivity {
+ final String githubURL = "https://github.com/adolfintel/OpenPods";
+ final String websiteURL = "https://fdossena.com/?p=openPods/i.frag";
+ final String donateURL = "https://paypal.me/sineisochronic";
+
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
- /**
+ /*
* LEGAL DANGER ZONE!
*
* The following actions are a violation of the GNU GPLv3 License:
@@ -32,26 +35,14 @@ public class AboutActivity extends AppCompatActivity {
((WebView)(findViewById(R.id.license))).loadUrl("file:///android_asset/license.html");
- ((Button)(findViewById(R.id.github))).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/adolfintel/OpenPods")));
- }
- });
- ((Button)(findViewById(R.id.website))).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://fdossena.com/?p=openPods/i.frag")));
- }
- });
- ((Button)(findViewById(R.id.donate))).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://paypal.me/sineisochronic")));
- Toast.makeText(AboutActivity.this, "❤️", Toast.LENGTH_SHORT).show();
- }
+ findViewById(R.id.github).setOnClickListener(v -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(githubURL))));
+ findViewById(R.id.website).setOnClickListener(v -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(websiteURL))));
+ findViewById(R.id.donate).setOnClickListener(v -> {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(donateURL)));
+ Toast.makeText(AboutActivity.this, "❤️", Toast.LENGTH_SHORT).show();
});
//END OF LEGAL DANGER ZONE
}
+
}
diff --git a/OpenPods/app/src/main/java/com/dosse/airpods/IntroActivity.java b/OpenPods/app/src/main/java/com/dosse/airpods/IntroActivity.java
index b796db4..a67d3ef 100644
--- a/OpenPods/app/src/main/java/com/dosse/airpods/IntroActivity.java
+++ b/OpenPods/app/src/main/java/com/dosse/airpods/IntroActivity.java
@@ -1,69 +1,78 @@
package com.dosse.airpods;
import android.Manifest;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.app.AppCompatActivity;
-import android.view.View;
-import android.widget.Button;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+
+import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
public class IntroActivity extends AppCompatActivity {
- private Timer t;
+ private Timer timer;
+
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_intro);
- ((Button)findViewById(R.id.allowBtn)).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) { //allow button clicked, ask for permissions
- requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1); //location (for BLE)
- try { //run in background
- if(!getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(getPackageName())) {
- Intent intent = new Intent();
- PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
- intent.setData(Uri.parse("package:" + getPackageName()));
- startActivity(intent);
- }
- } catch (Throwable t) {
+
+ // Allow button clicked, ask for permissions
+ findViewById(R.id.allowBtn).setOnClickListener(view -> {
+ requestPermissions(new String[] {Manifest.permission.ACCESS_COARSE_LOCATION}, 1); // Location (for BLE)
+ // Run in background
+ try {
+ if (!Objects.requireNonNull(getSystemService(PowerManager.class)).isIgnoringBatteryOptimizations(getPackageName())) {
+ Intent intent = new Intent();
+ getSystemService(Context.POWER_SERVICE);
+ intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ intent.setData(Uri.parse("package:" + getPackageName()));
+ startActivity(intent);
}
- }
+ } catch (Throwable ignored) { }
});
- //wait for permissions to be granted. When they are granted, go to MainActivity
- t= new Timer();
- t.scheduleAtFixedRate(new TimerTask() {
+
+ // Wait for permissions to be granted.
+ // When they are granted, go to MainActivity.
+ timer = new Timer();
+ timer.scheduleAtFixedRate(new TimerTask() {
@Override
- public void run() {
- boolean ok=true;
+ public void run () {
+ boolean ok = true;
+
try {
- if (!getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(getPackageName())) ok = false;
- }catch(Throwable t){}
- if (ContextCompat.checkSelfPermission(IntroActivity.this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) ok=false;
- if(ok){
- t.cancel();
- Intent i=new Intent(IntroActivity.this,MainActivity.class);
- startActivity(i);
+ if (!Objects.requireNonNull(getSystemService(PowerManager.class)).isIgnoringBatteryOptimizations(getPackageName()))
+ ok = false;
+ } catch (Throwable ignored) { }
+
+ if (ContextCompat.checkSelfPermission(IntroActivity.this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
+ ok = false;
+
+ if (ok) {
+ timer.cancel();
+ startActivity(new Intent(IntroActivity.this, MainActivity.class));
finish();
}
}
- },0,100);
+ }, 0, 100);
}
+ // Activity destroyed (or screen rotated). destroy the timer too
@Override
- protected void onDestroy() { //activity destroyed (or screen rotated). destroy the timer too
+ protected void onDestroy () {
super.onDestroy();
- if(t!=null) t.cancel();
+ if (timer != null)
+ timer.cancel();
}
+
}
diff --git a/OpenPods/app/src/main/java/com/dosse/airpods/MainActivity.java b/OpenPods/app/src/main/java/com/dosse/airpods/MainActivity.java
index 2b34c85..149635d 100644
--- a/OpenPods/app/src/main/java/com/dosse/airpods/MainActivity.java
+++ b/OpenPods/app/src/main/java/com/dosse/airpods/MainActivity.java
@@ -8,54 +8,73 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.PowerManager;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.app.AppCompatActivity;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+
+import java.util.Objects;
public class MainActivity extends AppCompatActivity {
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
- //check if Bluetooth LE is available on this device. If not, show an error
- BluetoothAdapter btAdapter=((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
- if(btAdapter==null||(btAdapter.isEnabled()&&btAdapter.getBluetoothLeScanner()==null)||(!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE))){
- Intent i=new Intent(this,NoBTActivity.class);
- startActivity(i);
+
+ // Check if Bluetooth LE is available on this device. If not, show an error
+ BluetoothAdapter btAdapter = ((BluetoothManager)Objects.requireNonNull(getSystemService(Context.BLUETOOTH_SERVICE))).getAdapter();
+ if (btAdapter == null || (btAdapter.isEnabled() && btAdapter.getBluetoothLeScanner() == null) || (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE))) {
+ startActivity(new Intent(MainActivity.this, NoBTActivity.class));
finish();
return;
}
- //check if all permissions have been granted
- boolean ok=true;
+
+ // Check if all permissions have been granted
+ boolean ok = true;
+
try {
- if (!getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(getPackageName())) ok = false;
- }catch(Throwable t){}
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) ok=false;
- if(ok){
+ if (!Objects.requireNonNull(getSystemService(PowerManager.class)).isIgnoringBatteryOptimizations(getPackageName()))
+ ok = false;
+ } catch (Throwable ignored) {
+ }
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
+ ok = false;
+
+ if (ok) {
Starter.startPodsService(getApplicationContext());
- }else{
- Intent i=new Intent(this,IntroActivity.class);
- startActivity(i);
+ } else {
+ startActivity(new Intent(MainActivity.this, IntroActivity.class));
finish();
}
- ((Button)(findViewById(R.id.settings))).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) { //settings clicked
- Intent i=new Intent(MainActivity.this, SettingsActivity.class);
- startActivity(i);
- }
- });
}
@Override
- protected void onResume() {
+ protected void onResume () {
super.onResume();
- try{
+ try {
getApplicationContext().openFileInput("hidden").close();
finish();
- }catch (Throwable t){}
+ } catch (Throwable ignored) {
+ }
}
+
+ @Override
+ public boolean onCreateOptionsMenu (Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected (@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.menu_ab_settings) {
+ startActivity(new Intent(this, SettingsActivity.class)); // Settings icon clicked
+ return true;
+ }
+ return false;
+ }
+
}
diff --git a/OpenPods/app/src/main/java/com/dosse/airpods/NoBTActivity.java b/OpenPods/app/src/main/java/com/dosse/airpods/NoBTActivity.java
index 61d7f9f..6c4f353 100644
--- a/OpenPods/app/src/main/java/com/dosse/airpods/NoBTActivity.java
+++ b/OpenPods/app/src/main/java/com/dosse/airpods/NoBTActivity.java
@@ -1,13 +1,15 @@
package com.dosse.airpods;
-import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+
public class NoBTActivity extends AppCompatActivity {
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_no_bt);
}
+
}
diff --git a/OpenPods/app/src/main/java/com/dosse/airpods/PodsService.java b/OpenPods/app/src/main/java/com/dosse/airpods/PodsService.java
index 93cda2b..bd06fbc 100644
--- a/OpenPods/app/src/main/java/com/dosse/airpods/PodsService.java
+++ b/OpenPods/app/src/main/java/com/dosse/airpods/PodsService.java
@@ -26,29 +26,31 @@ import android.os.IBinder;
import android.os.ParcelUuid;
import android.os.SystemClock;
import android.provider.Settings;
-import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
+
+import androidx.core.app.NotificationCompat;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
/**
* This is the class that does most of the work. It has 3 functions:
* - Detect when AirPods are detected
* - Receive beacons from AirPods and decode them (easier said than done thanks to google's autism)
* - Display the notification with the status
- *
*/
public class PodsService extends Service {
- private static final boolean ENABLE_LOGGING=BuildConfig.DEBUG; //Log is only displayed if this is a debug build, not release
+ private static final boolean ENABLE_LOGGING = BuildConfig.DEBUG; // Log is only displayed if this is a debug build, not release
private static BluetoothLeScanner btScanner;
- private static int leftStatus=15, rightStatus=15, caseStatus=15;
- private static boolean chargeL=false, chargeR=false, chargeCase=false;
- private static final String MODEL_AIRPODS_NORMAL="airpods12", MODEL_AIRPODS_PRO="airpodspro";
- private static String model=MODEL_AIRPODS_NORMAL;
+ private static int leftStatus = 15, rightStatus = 15, caseStatus = 15;
+ private static boolean chargeL = false, chargeR = false, chargeCase = false;
+ private static final String MODEL_AIRPODS_NORMAL = "airpods12", MODEL_AIRPODS_PRO = "airpodspro";
+ private static String model = MODEL_AIRPODS_NORMAL;
/**
* The following method (startAirPodsScanner) creates a bluetoth LE scanner.
@@ -56,7 +58,7 @@ public class PodsService extends Service {
* - Check that the beacon comes from something that looks like a pair of AirPods
* - Make sure that it is YOUR pair of AirPods
* - Decode the beacon to get the status
- *
+ *
* On a normal OS, we would use the bluetooth address of the device to filter out beacons from other devices.
* UNFORTUNATELY, someone at google was so concerned about privacy (yea, as if they give a shit) that he decided it was a good idea to not allow access to the bluetooth address of incoming BLE beacons. As a result, we have no reliable way to make sure that the beacon comes from YOUR airpods and not the guy sitting next to you on the bus.
* What we did to workaround this issue is this:
@@ -64,7 +66,7 @@ public class PodsService extends Service {
* - If the strongest beacon's fake address is the same as this, use this beacon; otherwise use the strongest beacon
* - Filter for signals stronger than -60db
* - Decode...
- *
+ *
* Decoding the beacon:
* This was done through reverse engineering. Hopefully it's correct.
* - The beacon coming from a pair of AirPods contains a manufacturer specific data field n°76 of 27 bytes
@@ -73,100 +75,134 @@ public class PodsService extends Service {
* - The 15th character in the string represents the charge of the case. Values between 0 and 10 are battery 0-100%; Value 15 means it's disconnected
* - The 14th character in the string represents the "in charge" status. Bit 0 (LSB) is the left pod; Bit 1 is the right pod; Bit 2 is the case. Bit 3 might be case open/closed but I'm not sure and it's not used
* - The 7th character in the string represents the AirPods model (E=AirPods pro)
- *
+ *
* After decoding a beacon, the status is written to leftStatus, rightStatus, caseStatus, chargeL, chargeR, chargeCase so that the NotificationThread can use the information
- *
*/
- private static ArrayList recentBeacons=new ArrayList<>();
- private static final long RECENT_BEACONS_MAX_T_NS=10000000000L; //10s
- private void startAirPodsScanner() {
+ private static ArrayList recentBeacons = new ArrayList<>();
+ private static final long RECENT_BEACONS_MAX_T_NS = 10000000000L; //10s
+
+ private void startAirPodsScanner () {
try {
- if(ENABLE_LOGGING) Log.d(TAG,"START SCANNER");
- SharedPreferences prefs=getSharedPreferences("openpods",MODE_PRIVATE);
- BluetoothManager btManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+ if (ENABLE_LOGGING)
+ Log.d(TAG, "START SCANNER");
+
+ SharedPreferences prefs = getSharedPreferences("openpods", MODE_PRIVATE);
+ BluetoothManager btManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
+ assert btManager != null;
BluetoothAdapter btAdapter = btManager.getAdapter();
- if(prefs.getBoolean("batterySaver",false)) {
+
+ if (prefs.getBoolean("batterySaver", false)) {
if (btScanner != null) {
btScanner.stopScan(new ScanCallback() {
@Override
- public void onScanResult(int callbackType, ScanResult result) {
+ public void onScanResult (int callbackType, ScanResult result) {
}
});
}
}
+
btScanner = btAdapter.getBluetoothLeScanner();
- if (btAdapter == null) throw new Exception("No BT");
- if (!btAdapter.isEnabled()) throw new Exception("BT Off");
+
+ if (!btAdapter.isEnabled())
+ throw new Exception("BT Off");
List filters = getScanFilters();
ScanSettings settings;
- if(prefs.getBoolean("batterySaver",false)) {
+
+ if (prefs.getBoolean("batterySaver", false))
settings = new ScanSettings.Builder().setScanMode(0).setReportDelay(0).build();
- }else{
+ else
settings = new ScanSettings.Builder().setScanMode(2).setReportDelay(2).build();
- }
btScanner.startScan(
filters,
settings,
new ScanCallback() {
- @Override
- public void onBatchScanResults(List scanResults) {
- for (ScanResult result : scanResults) onScanResult(-1, result);
- super.onBatchScanResults(scanResults);
- }
+ @Override
+ public void onBatchScanResults (List scanResults) {
+ for (ScanResult result : scanResults)
+ onScanResult(-1, result);
+ super.onBatchScanResults(scanResults);
+ }
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- try {
- byte[] data = result.getScanRecord().getManufacturerSpecificData(76);
- if (data == null||data.length!=27) return;
- recentBeacons.add(result);
- if(ENABLE_LOGGING) Log.d(TAG,""+result.getRssi()+"db");
- if(ENABLE_LOGGING) Log.d(TAG, decodeHex(data));
- ScanResult strongestBeacon=null;
- for(int i=0;iRECENT_BEACONS_MAX_T_NS){
- recentBeacons.remove(i--);
- continue;
+ @Override
+ public void onScanResult (int callbackType, ScanResult result) {
+ try {
+ byte[] data = Objects.requireNonNull(result.getScanRecord()).getManufacturerSpecificData(76);
+
+ if (data == null || data.length != 27)
+ return;
+
+ recentBeacons.add(result);
+
+ if (ENABLE_LOGGING) {
+ Log.d(TAG, "" + result.getRssi() + "db");
+ Log.d(TAG, decodeHex(data));
+ }
+
+ ScanResult strongestBeacon = null;
+ for (int i = 0; i < recentBeacons.size(); i++) {
+ if (SystemClock.elapsedRealtimeNanos() - recentBeacons.get(i).getTimestampNanos() > RECENT_BEACONS_MAX_T_NS) {
+ recentBeacons.remove(i--);
+ continue;
+ }
+ if (strongestBeacon == null || strongestBeacon.getRssi() < recentBeacons.get(i).getRssi())
+ strongestBeacon = recentBeacons.get(i);
+ }
+
+ if (strongestBeacon != null && strongestBeacon.getDevice().getAddress().equals(result.getDevice().getAddress()))
+ strongestBeacon = result;
+
+ result = strongestBeacon;
+ assert result != null;
+ if (result.getRssi() < -60)
+ return;
+
+ String a = decodeHex(Objects.requireNonNull(Objects.requireNonNull(result.getScanRecord()).getManufacturerSpecificData(76)));
+
+ String leftAirpodStr; // Left airpod (0-10 batt; 15=disconnected)
+ String rightAirpodStr; // Right airpod (0-10 batt; 15=disconnected)
+
+ if (isFlipped(a)) {
+ leftAirpodStr = "" + a.charAt(12);
+ rightAirpodStr = "" + a.charAt(13);
+ } else {
+ leftAirpodStr = "" + a.charAt(13);
+ rightAirpodStr = "" + a.charAt(12);
+ }
+
+ String caseStr = "" + a.charAt(15); // Case (0-10 batt; 15=disconnected)
+ String chargeStatusStr = "" + a.charAt(14); // Charge status (bit 0=left; bit 1=right; bit 2=case)
+
+ leftStatus = Integer.parseInt(leftAirpodStr, 16);
+ rightStatus = Integer.parseInt(rightAirpodStr, 16);
+ caseStatus = Integer.parseInt(caseStr, 16);
+
+ int chargeStatus = Integer.parseInt(chargeStatusStr, 16);
+
+ chargeL = (chargeStatus & 0b00000001) != 0;
+ chargeR = (chargeStatus & 0b00000010) != 0;
+ chargeCase = (chargeStatus & 0b00000100) != 0;
+
+ if (a.charAt(7) == 'E')
+ model = MODEL_AIRPODS_PRO;
+ else
+ model = MODEL_AIRPODS_NORMAL; // Detect if these are AirPods pro or regular ones
+
+ lastSeenConnected = System.currentTimeMillis();
+ } catch (Throwable t) {
+ if (ENABLE_LOGGING)
+ Log.d(TAG, "" + t);
}
- if(strongestBeacon==null||strongestBeacon.getRssi() getScanFilters() {
+ private List getScanFilters () {
byte[] manufacturerData = new byte[27];
byte[] manufacturerDataMask = new byte[27];
@@ -178,66 +214,81 @@ public class PodsService extends Service {
Builder builder = new Builder();
builder.setManufacturerData(76, manufacturerData, manufacturerDataMask);
+
return Collections.singletonList(builder.build());
}
- private void stopAirPodsScanner(){
- try{
- if(btScanner!=null){
- if(ENABLE_LOGGING) Log.d(TAG,"STOP SCANNER");
+ private void stopAirPodsScanner () {
+ try {
+ if (btScanner != null) {
+ if (ENABLE_LOGGING)
+ Log.d(TAG, "STOP SCANNER");
+
btScanner.stopScan(new ScanCallback() {
@Override
- public void onScanResult(int callbackType, ScanResult result) {}});
+ public void onScanResult (int callbackType, ScanResult result) {
+ }
+ });
}
- leftStatus=15; rightStatus=15; caseStatus=15;
- }catch (Throwable t){}
+ leftStatus = 15;
+ rightStatus = 15;
+ caseStatus = 15;
+ } catch (Throwable ignored) {
+ }
}
- private final char[] hexCharset = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
- private String decodeHex(byte[] bArr) {
+ private final char[] hexCharset = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+ private String decodeHex (byte[] bArr) {
char[] ret = new char[bArr.length * 2];
+
for (int i = 0; i < bArr.length; i++) {
int b = bArr[i] & 0xFF;
- ret[i*2] = hexCharset[b >>> 4];
- ret[i*2+1] = hexCharset[b & 0x0F];
+ ret[i * 2] = hexCharset[b >>> 4];
+ ret[i * 2 + 1] = hexCharset[b & 0x0F];
}
+
return new String(ret);
}
- private boolean isFlipped(String str) {
- return (Integer.toString(Integer.parseInt(""+str.charAt(10),16)+0x10,2)).charAt(3)=='0';
+ private boolean isFlipped (String str) {
+ return (Integer.toString(Integer.parseInt("" + str.charAt(10), 16) + 0x10, 2)).charAt(3) == '0';
}
/**
* The following class is a thread that manages the notification while your AirPods are connected.
- *
+ *
* It simply reads the status variables every 1 seconds and creates, destroys, or updates the notification accordingly.
* The notification is shown when BT is on and AirPods are connected. The status is updated every 1 second. Battery% is hidden if we didn't receive a beacon for 30 seconds (screen off for a while)
- *
+ *
* This thread is the reason why we need permission to disable doze. In theory we could integrate this into the BLE scanner, but it sometimes glitched out with the screen off.
- *
*/
- private static NotificationThread n=null;
- private static final String TAG="AirPods";
- private static long lastSeenConnected=0;
- private static final long TIMEOUT_CONNECTED=30000;
- private static boolean maybeConnected =false;
- private class NotificationThread extends Thread{
- private boolean isLocationEnabled(){
- if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P){
- LocationManager service = (LocationManager) getSystemService(getApplicationContext().LOCATION_SERVICE);
- return service!=null&&service.isLocationEnabled();
- }else{
+ private static NotificationThread n = null;
+ private static final String TAG = "AirPods";
+ private static long lastSeenConnected = 0;
+ private static final long TIMEOUT_CONNECTED = 30000;
+ private static boolean maybeConnected = false;
+
+ private class NotificationThread extends Thread {
+ private boolean isLocationEnabled () {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ getApplicationContext();
+ LocationManager service = (LocationManager)getSystemService(LOCATION_SERVICE);
+ return service != null && service.isLocationEnabled();
+ } else {
try {
return Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE) != Settings.Secure.LOCATION_MODE_OFF;
- }catch(Throwable t){
+ } catch (Throwable t) {
return true;
}
}
}
+
private NotificationManager mNotifyManager;
- public NotificationThread(){
- mNotifyManager=(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+
+ @SuppressWarnings("WeakerAccess")
+ public NotificationThread () {
+ mNotifyManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //on oreo and newer, create a notification channel
NotificationChannel channel = new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
channel.enableVibration(false);
@@ -247,48 +298,61 @@ public class PodsService extends Service {
mNotifyManager.createNotificationChannel(channel);
}
}
- public void run(){
- boolean notificationShowing=false;
- RemoteViews notificationBig=new RemoteViews(getPackageName(),R.layout.status_big);
- RemoteViews notificationSmall=new RemoteViews(getPackageName(),R.layout.status_small);
- RemoteViews locationDisabledBig=new RemoteViews(getPackageName(),R.layout.location_disabled_big);
- RemoteViews locationDisabledSmall=new RemoteViews(getPackageName(),R.layout.location_disabled_small);
- NotificationCompat.Builder mBuilder=new NotificationCompat.Builder(PodsService.this,TAG);
+
+ public void run () {
+ boolean notificationShowing = false;
+
+ RemoteViews notificationBig = new RemoteViews(getPackageName(), R.layout.status_big);
+ RemoteViews notificationSmall = new RemoteViews(getPackageName(), R.layout.status_small);
+ RemoteViews locationDisabledBig = new RemoteViews(getPackageName(), R.layout.location_disabled_big);
+ RemoteViews locationDisabledSmall = new RemoteViews(getPackageName(), R.layout.location_disabled_small);
+
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(PodsService.this, TAG);
mBuilder.setShowWhen(false);
mBuilder.setOngoing(true);
mBuilder.setSmallIcon(R.mipmap.notification_icon);
- for(;;){
- if(maybeConnected &&!(leftStatus==15&&rightStatus==15&&caseStatus==15)/*&&System.currentTimeMillis()-lastSeenConnected=29) { //apparently this restriction was removed in android Q
+
+ // Apparently this restriction was removed in android Q
+ if (isLocationEnabled() || Build.VERSION.SDK_INT >= 29) {
mBuilder.setCustomContentView(notificationSmall);
mBuilder.setCustomBigContentView(notificationBig);
- }else{
+ } else {
mBuilder.setCustomContentView(locationDisabledSmall);
mBuilder.setCustomBigContentView(locationDisabledBig);
}
- if(notificationShowing){
- if(ENABLE_LOGGING) Log.d(TAG,"Left: "+leftStatus+(chargeL?"+":"")+" "+"Right: "+rightStatus+(chargeR?"+":"")+" "+"Case: "+caseStatus+(chargeCase?"+":"")+" "+"Model: "+model);
- if(model.equals(MODEL_AIRPODS_NORMAL)){
+
+ if (notificationShowing) {
+ if (ENABLE_LOGGING)
+ Log.d(TAG, "Left: " + leftStatus + (chargeL ? "+" : "") + " Right: " + rightStatus + (chargeR ? "+" : "") + " Case: " + caseStatus + (chargeCase ? "+" : "") + " Model: " + model);
+
+ if (model.equals(MODEL_AIRPODS_NORMAL)) {
notificationBig.setImageViewResource(R.id.leftPodImg, leftStatus <= 10 ? R.drawable.left_pod : R.drawable.left_pod_disconnected);
notificationBig.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.right_pod : R.drawable.right_pod_disconnected);
notificationBig.setImageViewResource(R.id.podCaseImg, caseStatus <= 10 ? R.drawable.pod_case : R.drawable.pod_case_disconnected);
notificationSmall.setImageViewResource(R.id.leftPodImg, leftStatus <= 10 ? R.drawable.left_pod : R.drawable.left_pod_disconnected);
notificationSmall.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.right_pod : R.drawable.right_pod_disconnected);
notificationSmall.setImageViewResource(R.id.podCaseImg, caseStatus <= 10 ? R.drawable.pod_case : R.drawable.pod_case_disconnected);
- }else if(model.equals(MODEL_AIRPODS_PRO)){
+ } else if (model.equals(MODEL_AIRPODS_PRO)) {
notificationBig.setImageViewResource(R.id.leftPodImg, leftStatus <= 10 ? R.drawable.left_podpro : R.drawable.left_podpro_disconnected);
notificationBig.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.right_podpro : R.drawable.right_podpro_disconnected);
notificationBig.setImageViewResource(R.id.podCaseImg, caseStatus <= 10 ? R.drawable.podpro_case : R.drawable.podpro_case_disconnected);
@@ -296,7 +360,8 @@ public class PodsService extends Service {
notificationSmall.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.right_podpro : R.drawable.right_podpro_disconnected);
notificationSmall.setImageViewResource(R.id.podCaseImg, caseStatus <= 10 ? R.drawable.podpro_case : R.drawable.podpro_case_disconnected);
}
- if(System.currentTimeMillis()-lastSeenConnected {
+ startActivity(new Intent(context, AboutActivity.class));
+ return true;
+ });
+
+ mHideAppPrefernce = getPreferenceManager().findPreference("hideApp");
+ Objects.requireNonNull(mHideAppPrefernce).setOnPreferenceClickListener(preference -> {
+ PackageManager p = requireContext().getPackageManager();
+ p.setComponentEnabledSetting(new ComponentName(context, MainActivity.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
+ Toast.makeText(context, getString(R.string.hideClicked), Toast.LENGTH_LONG).show();
+
+ try {
+ context.openFileOutput("hidden", Context.MODE_PRIVATE).close();
+ } catch (Throwable ignored) {
+ }
+
+ enableDisableOptions();
+ requireActivity().finish();
+ return true;
+ });
+
+ enableDisableOptions();
+ }
+
+ private void enableDisableOptions () {
+ try {
+ context.openFileInput("hidden").close();
+ mHideAppPrefernce.setEnabled(false);
+ } catch (Throwable ignored) {
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/OpenPods/app/src/main/res/drawable/ic_check_dgrey_48dp.xml b/OpenPods/app/src/main/res/drawable/ic_check_dgrey_48dp.xml
new file mode 100644
index 0000000..73ee4ab
--- /dev/null
+++ b/OpenPods/app/src/main/res/drawable/ic_check_dgrey_48dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/OpenPods/app/src/main/res/drawable/ic_menu_settings_white_30dp.xml b/OpenPods/app/src/main/res/drawable/ic_menu_settings_white_30dp.xml
new file mode 100644
index 0000000..02be8c7
--- /dev/null
+++ b/OpenPods/app/src/main/res/drawable/ic_menu_settings_white_30dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/OpenPods/app/src/main/res/drawable/ic_x_dgrey_48dp.xml b/OpenPods/app/src/main/res/drawable/ic_x_dgrey_48dp.xml
new file mode 100644
index 0000000..f518c16
--- /dev/null
+++ b/OpenPods/app/src/main/res/drawable/ic_x_dgrey_48dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/OpenPods/app/src/main/res/drawable/tick.png b/OpenPods/app/src/main/res/drawable/tick.png
deleted file mode 100644
index 27ee5a8..0000000
Binary files a/OpenPods/app/src/main/res/drawable/tick.png and /dev/null differ
diff --git a/OpenPods/app/src/main/res/drawable/x.png b/OpenPods/app/src/main/res/drawable/x.png
deleted file mode 100644
index 6ad6fa0..0000000
Binary files a/OpenPods/app/src/main/res/drawable/x.png and /dev/null differ
diff --git a/OpenPods/app/src/main/res/layout/activity_about.xml b/OpenPods/app/src/main/res/layout/activity_about.xml
index 48c4a23..cf20bc8 100644
--- a/OpenPods/app/src/main/res/layout/activity_about.xml
+++ b/OpenPods/app/src/main/res/layout/activity_about.xml
@@ -3,17 +3,17 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".AboutActivity"
- android:padding="12dp"
android:background="#FFFFFF"
- >
+ android:padding="12dp"
+ tools:context=".AboutActivity"
+ tools:ignore="ContentDescription,ButtonStyle">
+ android:layout_marginBottom="16dp"
+ android:src="@drawable/logo" />
+ android:gravity="center"
+ android:text="@string/about1"
+ android:textColor="#404040" />
+ android:gravity="center">
+
+ android:text="@string/website" />
+
+ android:text="@string/github" />
+
+ android:text="@string/donate" />
+
+ android:layout_below="@id/buttons" />
diff --git a/OpenPods/app/src/main/res/layout/activity_intro.xml b/OpenPods/app/src/main/res/layout/activity_intro.xml
index a030b46..81a2328 100644
--- a/OpenPods/app/src/main/res/layout/activity_intro.xml
+++ b/OpenPods/app/src/main/res/layout/activity_intro.xml
@@ -1,59 +1,51 @@
-
+
-
-
+ android:orientation="vertical"
+ android:padding="12dp">
+ android:src="@drawable/left_pod"
+ tools:ignore="ContentDescription" />
+
+ android:text="@string/intro2" />
+ android:text="@string/introAllow" />
-
+
-
-
+
diff --git a/OpenPods/app/src/main/res/layout/activity_main.xml b/OpenPods/app/src/main/res/layout/activity_main.xml
index df4c551..6cc8830 100644
--- a/OpenPods/app/src/main/res/layout/activity_main.xml
+++ b/OpenPods/app/src/main/res/layout/activity_main.xml
@@ -1,58 +1,49 @@
-
+ tools:ignore="ContentDescription">
-
+
+ android:scaleType="fitCenter"
+ android:scaleX="1.1"
+ android:scaleY="1.1"
+ android:src="@drawable/ic_check_dgrey_48dp" />
+
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large" />
+
+
-
+ android:text="@string/main2"
+ android:textStyle="italic" />
-
+
-
+
diff --git a/OpenPods/app/src/main/res/layout/activity_no_bt.xml b/OpenPods/app/src/main/res/layout/activity_no_bt.xml
index eafc999..4d56606 100644
--- a/OpenPods/app/src/main/res/layout/activity_no_bt.xml
+++ b/OpenPods/app/src/main/res/layout/activity_no_bt.xml
@@ -1,32 +1,35 @@
-
+
-
+ android:orientation="vertical"
+ android:padding="12dp">
+ android:scaleType="fitCenter"
+ android:scaleX="1.6"
+ android:scaleY="1.6"
+ android:src="@drawable/ic_x_dgrey_48dp"
+ tools:ignore="ContentDescription" />
+ android:text="@string/noBtText"
+ android:textColor="#000"
+ android:textSize="15sp"
+ android:textStyle="" />
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/OpenPods/app/src/main/res/layout/activity_settings.xml b/OpenPods/app/src/main/res/layout/activity_settings.xml
index 33cc36a..2d41426 100644
--- a/OpenPods/app/src/main/res/layout/activity_settings.xml
+++ b/OpenPods/app/src/main/res/layout/activity_settings.xml
@@ -1,8 +1,7 @@
-
-
-
\ No newline at end of file
+ tools:context=".SettingsActivity" />
\ No newline at end of file
diff --git a/OpenPods/app/src/main/res/layout/location_disabled_big.xml b/OpenPods/app/src/main/res/layout/location_disabled_big.xml
index 10ba2a6..21363ea 100644
--- a/OpenPods/app/src/main/res/layout/location_disabled_big.xml
+++ b/OpenPods/app/src/main/res/layout/location_disabled_big.xml
@@ -1,18 +1,16 @@
+ android:gravity="center"
+ android:padding="12dp">
+ android:textColor="@color/black" />
diff --git a/OpenPods/app/src/main/res/layout/location_disabled_small.xml b/OpenPods/app/src/main/res/layout/location_disabled_small.xml
index b623f52..1f5b573 100644
--- a/OpenPods/app/src/main/res/layout/location_disabled_small.xml
+++ b/OpenPods/app/src/main/res/layout/location_disabled_small.xml
@@ -1,18 +1,16 @@
+ android:gravity="center"
+ android:padding="12dp">
+ android:textColor="@color/black" />
diff --git a/OpenPods/app/src/main/res/layout/status_big.xml b/OpenPods/app/src/main/res/layout/status_big.xml
index 36072bb..05a7475 100644
--- a/OpenPods/app/src/main/res/layout/status_big.xml
+++ b/OpenPods/app/src/main/res/layout/status_big.xml
@@ -1,39 +1,36 @@
-
+ android:gravity="center"
+ android:padding="12dp"
+ tools:ignore="RtlHardcoded,ContentDescription">
+ android:layout_alignParentTop="true">
+ android:src="@drawable/left_pod" />
+ android:textColor="@color/black" />
+ android:indeterminate="true" />
@@ -59,19 +55,17 @@
android:layout_width="40dp"
android:layout_height="90dp"
android:layout_centerHorizontal="true"
- android:src="@drawable/right_pod"
android:layout_marginBottom="6dp"
- />
+ android:src="@drawable/right_pod" />
+ android:textColor="@color/black" />
+ android:indeterminate="true" />
@@ -97,19 +90,17 @@
android:layout_width="80dp"
android:layout_height="90dp"
android:layout_centerHorizontal="true"
- android:src="@drawable/pod_case"
android:layout_marginBottom="6dp"
- />
+ android:src="@drawable/pod_case" />
+ android:textColor="@color/black" />
+ android:indeterminate="true" />
diff --git a/OpenPods/app/src/main/res/layout/status_small.xml b/OpenPods/app/src/main/res/layout/status_small.xml
index b513de4..62d67e6 100644
--- a/OpenPods/app/src/main/res/layout/status_small.xml
+++ b/OpenPods/app/src/main/res/layout/status_small.xml
@@ -1,37 +1,35 @@
+ android:gravity="center"
+ android:padding="12dp"
+ tools:ignore="RtlHardcoded,ContentDescription">
+ android:layout_alignParentTop="true">
+ android:src="@drawable/left_pod" />
+ android:textColor="@color/black" />
+ android:indeterminate="true" />
+ android:layout_toRightOf="@id/leftPod">
+ android:src="@drawable/right_pod" />
+ android:textColor="@color/black" />
+ android:indeterminate="true" />
+ android:layout_toRightOf="@id/rightPod">
+ android:src="@drawable/pod_case" />
+ android:textColor="@color/black" />
+ android:indeterminate="true" />
diff --git a/OpenPods/app/src/main/res/menu/menu_main.xml b/OpenPods/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..46ab068
--- /dev/null
+++ b/OpenPods/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/OpenPods/app/src/main/res/xml/pref_general.xml b/OpenPods/app/src/main/res/xml/preference_screen.xml
similarity index 71%
rename from OpenPods/app/src/main/res/xml/pref_general.xml
rename to OpenPods/app/src/main/res/xml/preference_screen.xml
index 98db7d7..4b09ed9 100644
--- a/OpenPods/app/src/main/res/xml/pref_general.xml
+++ b/OpenPods/app/src/main/res/xml/preference_screen.xml
@@ -1,17 +1,18 @@
+
+ android:key="batterySaver"
+ android:summary="@string/batterySaver_desc"
+ android:title="@string/batterySaver" />
+
+ android:title="@string/hide" />
+
+ android:title="@string/about" />
+
diff --git a/OpenPods/build.gradle b/OpenPods/build.gradle
index 2afeee3..d29fea0 100644
--- a/OpenPods/build.gradle
+++ b/OpenPods/build.gradle
@@ -7,7 +7,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.6.0'
+ classpath 'com.android.tools.build:gradle:3.6.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/OpenPods/gradle.properties b/OpenPods/gradle.properties
index 82618ce..d546dea 100644
--- a/OpenPods/gradle.properties
+++ b/OpenPods/gradle.properties
@@ -6,6 +6,8 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
+android.enableJetifier=true
+android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
diff --git a/OpenPods/gradle/wrapper/gradle-wrapper.properties b/OpenPods/gradle/wrapper/gradle-wrapper.properties
index 9bf543c..f351fcf 100644
--- a/OpenPods/gradle/wrapper/gradle-wrapper.properties
+++ b/OpenPods/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip