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"> +