11 Commits
Author SHA1 Message Date
adolfintel 21db852661 Release Alpha 3 2019-03-18 12:10:38 +01:00
adolfintel c11e261bba Aesthetic changes 2019-03-18 12:07:15 +01:00
adolfintel cff007f161 Added warning if location disabled 2019-03-16 10:48:05 +01:00
adolfintel 18be725cf3 Improved looks of notification 2019-03-15 19:31:09 +01:00
adolfintel df90f89785 Fixed a problem in BLE detection 2019-03-14 15:26:55 +01:00
adolfintel d08c5a2c22 Release 0.2 (Alpha 2) 2019-03-14 12:42:42 +01:00
adolfintel 2b66a73874 Minor bug fixes; Documented PodsService 2019-03-14 09:47:17 +01:00
adolfintel af673910b0 Updated strings 2019-03-14 08:50:22 +01:00
adolfintel 2e0d0347c1 Improved Bluetooth LE detection 2019-03-14 08:48:29 +01:00
adolfintel 5e55d0bdc0 Improved bluetooth scanner 2019-03-14 08:43:33 +01:00
adolfintel d864b62efd Added donation link 2019-03-13 06:46:27 +01:00
16 changed files with 283 additions and 54 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 59 KiB

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+4 -2
View File
@@ -6,8 +6,8 @@ android {
applicationId "com.dosse.airpods" applicationId "com.dosse.airpods"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 28 targetSdkVersion 28
versionCode 1 versionCode 3
versionName "1.0" versionName '0.3'
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
@@ -16,6 +16,8 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
productFlavors {
}
} }
dependencies { dependencies {
@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-feature <uses-feature
android:name="android.hardware.bluetooth_le" android:name="android.hardware.bluetooth_le"
@@ -26,9 +26,9 @@ public class IntroActivity extends AppCompatActivity {
setContentView(R.layout.activity_intro); setContentView(R.layout.activity_intro);
((Button)findViewById(R.id.allowBtn)).setOnClickListener(new View.OnClickListener() { ((Button)findViewById(R.id.allowBtn)).setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) { //allow button clicked, ask for permissions
requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1); requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1); //location (for BLE)
try { try { //run in background
if(!getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(getPackageName())) { if(!getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(getPackageName())) {
Intent intent = new Intent(); Intent intent = new Intent();
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
@@ -40,14 +40,15 @@ public class IntroActivity extends AppCompatActivity {
} }
} }
}); });
//wait for permissions to be granted. When they are granted, go to MainActivity
t= new Timer(); t= new Timer();
t.scheduleAtFixedRate(new TimerTask() { t.scheduleAtFixedRate(new TimerTask() {
@Override @Override
public void run() { public void run() {
boolean ok=true; boolean ok=true;
if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) { try {
if(!getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(getPackageName())) ok=false; 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 (ContextCompat.checkSelfPermission(IntroActivity.this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) ok=false;
if(ok){ if(ok){
t.cancel(); t.cancel();
@@ -61,7 +62,7 @@ public class IntroActivity extends AppCompatActivity {
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() { //activity destroyed (or screen rotated). destroy the timer too
super.onDestroy(); super.onDestroy();
if(t!=null) t.cancel(); if(t!=null) t.cancel();
} }
@@ -1,6 +1,8 @@
package com.dosse.airpods; package com.dosse.airpods;
import android.Manifest; import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@@ -21,12 +23,15 @@ public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
if(((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter()==null){ //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)){
Intent i=new Intent(this,NoBTActivity.class); Intent i=new Intent(this,NoBTActivity.class);
startActivity(i); startActivity(i);
finish(); finish();
return; return;
} }
//check if all permissions have been granted
boolean ok=true; boolean ok=true;
try { try {
if (!getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(getPackageName())) ok = false; if (!getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(getPackageName())) ok = false;
@@ -41,11 +46,11 @@ public class MainActivity extends AppCompatActivity {
} }
((Button)(findViewById(R.id.mainHide))).setOnClickListener(new View.OnClickListener() { ((Button)(findViewById(R.id.mainHide))).setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) { //hide app clicked
PackageManager p = getPackageManager(); PackageManager p = getPackageManager();
p.setComponentEnabledSetting(new ComponentName(MainActivity.this,MainActivity.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); p.setComponentEnabledSetting(new ComponentName(MainActivity.this,MainActivity.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
Toast.makeText(getApplicationContext(),getString(R.string.hideClicked), Toast.LENGTH_LONG).show(); Toast.makeText(getApplicationContext(),getString(R.string.hideClicked), Toast.LENGTH_LONG).show();
try{getApplicationContext().openFileOutput("hidden",MODE_PRIVATE).close();}catch(Throwable t){} finish();
} }
}); });
} }
@@ -13,23 +13,60 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.location.LocationManager;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.ParcelUuid; import android.os.ParcelUuid;
import android.os.SystemClock;
import android.provider.Settings;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import java.util.ArrayList;
/**
* 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 do google's autism)
* - Display the notification with the status
*
*/
public class PodsService extends Service { 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 NotificationThread n=null;
private static BluetoothLeScanner btScanner; private static BluetoothLeScanner btScanner;
private static int leftStatus=15, rightStatus=15, caseStatus=15; private static int leftStatus=15, rightStatus=15, caseStatus=15;
private static boolean chargeL=false, chargeR=false, chargeCase=false; private static boolean chargeL=false, chargeR=false, chargeCase=false;
private static long lastSeenConnected=0;
private static final long TIMEOUT_CONNECTED=30000;
private static boolean maybeConnected =true;
/**
* The following method (startAirPodsScanner) creates a bluetoth LE scanner.
* This scanner receives all beacons from nearby BLE devices (not just your devices!) so we need to do 3 things:
* - 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:
* - When a beacon arrives that looks like a pair of AirPods, look at the other beacons received in the last 10 seconds and get the strongest one
* - If the strongest beacon's fake address is the same as this, use this beacon; otherwise use the strongest beacon
* - 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
* - We convert this data to a hexadecimal string
* - The 12th and 13th characters in the string represent the charge of the left and right pods. Under unknown circumstances, they are right and left instead (see isFlipped). Values between 0 and 10 are battery 0-100%; Value 15 means it's disconnected
* - 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
*
* 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<ScanResult> recentBeacons=new ArrayList<>();
private static final long RECENT_BEACONS_MAX_T_NS=10000000000L; //10s
private void startAirPodsScanner() { private void startAirPodsScanner() {
try { try {
BluetoothManager btManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); BluetoothManager btManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
@@ -42,9 +79,19 @@ public class PodsService extends Service {
public void onScanResult(int callbackType, ScanResult result) { public void onScanResult(int callbackType, ScanResult result) {
try { try {
byte[] data = result.getScanRecord().getManufacturerSpecificData(76); byte[] data = result.getScanRecord().getManufacturerSpecificData(76);
if (data == null) return; if (data == null||data.length!=27) return;
String a = decodeHex(data); recentBeacons.add(result);
if (a.length() < 16) return; 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;
String a=decodeHex(result.getScanRecord().getManufacturerSpecificData(76));
String str = ""; //left airpod (0-10 batt; 15=disconnected) String str = ""; //left airpod (0-10 batt; 15=disconnected)
String str2 = ""; //right airpod (0-10 batt; 15=disconnected) String str2 = ""; //right airpod (0-10 batt; 15=disconnected)
if (isFlipped(a)) { if (isFlipped(a)) {
@@ -65,15 +112,26 @@ public class PodsService extends Service {
chargeCase = (chargeStatus & 0b00000100) != 0; chargeCase = (chargeStatus & 0b00000100) != 0;
lastSeenConnected = System.currentTimeMillis(); lastSeenConnected = System.currentTimeMillis();
} catch (Throwable t) { } catch (Throwable t) {
Log.d("PODS", "" + t); if(ENABLE_LOGGING) Log.d(TAG, "" + t);
} }
} }
}); });
} catch (Throwable t) { } catch (Throwable t) {
Log.d("PODS", "" + t); if(ENABLE_LOGGING) Log.d(TAG, "" + t);
} }
} }
private void stopAirPodsScanner(){
try{
if(btScanner!=null){
btScanner.stopScan(new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {}});
}
leftStatus=15; rightStatus=15; caseStatus=15;
}catch (Throwable t){}
}
private final char[] hexCharset = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; 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 String decodeHex(byte[] bArr) {
char[] ret = new char[bArr.length * 2]; char[] ret = new char[bArr.length * 2];
@@ -89,12 +147,31 @@ public class PodsService extends Service {
return (Integer.toString(Integer.parseInt(""+str.charAt(10),16)+0x10,2)).charAt(3)=='0'; 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 final String TAG="AirPods";
private static long lastSeenConnected=0;
private static final long TIMEOUT_CONNECTED=30000;
private static boolean maybeConnected =true;
private class NotificationThread extends Thread{ private class NotificationThread extends Thread{
private boolean isLocationEnabled(){
LocationManager service = (LocationManager) getSystemService(LOCATION_SERVICE);
return service!=null&&service.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}
public void run(){ public void run(){
boolean notificationShowing=false; boolean notificationShowing=false;
RemoteViews notificationBig=new RemoteViews(getPackageName(),R.layout.status_big); RemoteViews notificationBig=new RemoteViews(getPackageName(),R.layout.status_big);
RemoteViews notificationSmall=new RemoteViews(getPackageName(),R.layout.status_small); 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);
NotificationManager mNotifyManager=(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager mNotifyManager=(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder mBuilder=new NotificationCompat.Builder(PodsService.this,TAG); NotificationCompat.Builder mBuilder=new NotificationCompat.Builder(PodsService.this,TAG);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //on oreo and newer, create a notification channel if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //on oreo and newer, create a notification channel
@@ -104,31 +181,48 @@ public class PodsService extends Service {
channel.setShowBadge(true); channel.setShowBadge(true);
mNotifyManager.createNotificationChannel(channel); mNotifyManager.createNotificationChannel(channel);
} }
mBuilder.setCustomContentView(notificationSmall);
mBuilder.setCustomBigContentView(notificationBig);
mBuilder.setShowWhen(false); mBuilder.setShowWhen(false);
mBuilder.setOngoing(true); mBuilder.setOngoing(true);
mBuilder.setSmallIcon(R.drawable.left_pod); mBuilder.setSmallIcon(R.drawable.left_pod);
for(;;){ for(;;){
if(maybeConnected &&!(leftStatus==15&&rightStatus==15&&caseStatus==15)){ if(maybeConnected &&!(leftStatus==15&&rightStatus==15&&caseStatus==15)){
if(!notificationShowing){ if(!notificationShowing){
Log.d("PODS","Creating notification"); if(ENABLE_LOGGING) Log.d(TAG,"Creating notification");
notificationShowing=true; notificationShowing=true;
mNotifyManager.notify(1,mBuilder.build()); mNotifyManager.notify(1,mBuilder.build());
} }
}else{ }else{
if(notificationShowing){ if(notificationShowing){
Log.d("PODS","Removing notification"); if(ENABLE_LOGGING) Log.d(TAG,"Removing notification");
notificationShowing=false; notificationShowing=false;
mNotifyManager.cancel(1); mNotifyManager.cancel(1);
} }
} }
if(isLocationEnabled()) {
mBuilder.setCustomContentView(notificationSmall);
mBuilder.setCustomBigContentView(notificationBig);
}else{
mBuilder.setCustomContentView(locationDisabledSmall);
mBuilder.setCustomBigContentView(locationDisabledBig);
}
if(notificationShowing){ if(notificationShowing){
Log.d("PODS","Left: "+leftStatus+(chargeL?"+":"")+" "+"Right: "+rightStatus+(chargeR?"+":"")+" "+"Case: "+caseStatus+(chargeCase?"+":"")); if(ENABLE_LOGGING) Log.d(TAG,"Left: "+leftStatus+(chargeL?"+":"")+" "+"Right: "+rightStatus+(chargeR?"+":"")+" "+"Case: "+caseStatus+(chargeCase?"+":""));
notificationBig.setImageViewResource(R.id.leftPodImg,leftStatus<=10?R.drawable.left_pod:R.drawable.left_pod_disconnected); 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.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); notificationBig.setImageViewResource(R.id.podCaseImg,caseStatus<=10?R.drawable.pod_case:R.drawable.pod_case_disconnected);
if(System.currentTimeMillis()-lastSeenConnected<TIMEOUT_CONNECTED) { if(System.currentTimeMillis()-lastSeenConnected<TIMEOUT_CONNECTED) {
notificationBig.setViewVisibility(R.id.leftPodText, View.VISIBLE);
notificationBig.setViewVisibility(R.id.rightPodText, View.VISIBLE);
notificationBig.setViewVisibility(R.id.podCaseText, View.VISIBLE);
notificationBig.setViewVisibility(R.id.leftPodUpdating, View.INVISIBLE);
notificationBig.setViewVisibility(R.id.rightPodUpdating, View.INVISIBLE);
notificationBig.setViewVisibility(R.id.podCaseUpdating, View.INVISIBLE);
notificationSmall.setViewVisibility(R.id.leftPodText, View.VISIBLE);
notificationSmall.setViewVisibility(R.id.rightPodText, View.VISIBLE);
notificationSmall.setViewVisibility(R.id.podCaseText, View.VISIBLE);
notificationSmall.setViewVisibility(R.id.leftPodUpdating, View.INVISIBLE);
notificationSmall.setViewVisibility(R.id.rightPodUpdating, View.INVISIBLE);
notificationSmall.setViewVisibility(R.id.podCaseUpdating, View.INVISIBLE);
notificationBig.setTextViewText(R.id.leftPodText, (leftStatus <= 10 ? (leftStatus * 10) + "%" : "") + ((chargeL && leftStatus < 10) ? "+" : "")); notificationBig.setTextViewText(R.id.leftPodText, (leftStatus <= 10 ? (leftStatus * 10) + "%" : "") + ((chargeL && leftStatus < 10) ? "+" : ""));
notificationBig.setTextViewText(R.id.rightPodText, (rightStatus <= 10 ? (rightStatus * 10) + "%" : "") + ((chargeR && rightStatus < 10) ? "+" : "")); notificationBig.setTextViewText(R.id.rightPodText, (rightStatus <= 10 ? (rightStatus * 10) + "%" : "") + ((chargeR && rightStatus < 10) ? "+" : ""));
notificationBig.setTextViewText(R.id.podCaseText, (caseStatus <= 10 ? (caseStatus * 10) + "%" : "") + (chargeCase ? "+" : "")); notificationBig.setTextViewText(R.id.podCaseText, (caseStatus <= 10 ? (caseStatus * 10) + "%" : "") + (chargeCase ? "+" : ""));
@@ -136,27 +230,28 @@ public class PodsService extends Service {
notificationSmall.setTextViewText(R.id.rightPodText, (rightStatus <= 10 ? (rightStatus * 10) + "%" : "") + (chargeR ? "+" : "")); notificationSmall.setTextViewText(R.id.rightPodText, (rightStatus <= 10 ? (rightStatus * 10) + "%" : "") + (chargeR ? "+" : ""));
notificationSmall.setTextViewText(R.id.podCaseText, (caseStatus <= 10 ? (caseStatus * 10) + "%" : "") + (chargeCase ? "+" : "")); notificationSmall.setTextViewText(R.id.podCaseText, (caseStatus <= 10 ? (caseStatus * 10) + "%" : "") + (chargeCase ? "+" : ""));
}else{ }else{
//haven't received an update in a while (screen off), wait for an update before showing battery% notificationBig.setViewVisibility(R.id.leftPodText, View.INVISIBLE);
notificationBig.setTextViewText(R.id.leftPodText, ""); notificationBig.setViewVisibility(R.id.rightPodText, View.INVISIBLE);
notificationBig.setTextViewText(R.id.rightPodText, ""); notificationBig.setViewVisibility(R.id.podCaseText, View.INVISIBLE);
notificationBig.setTextViewText(R.id.podCaseText, ""); notificationBig.setViewVisibility(R.id.leftPodUpdating, View.VISIBLE);
notificationSmall.setTextViewText(R.id.leftPodText, ""); notificationBig.setViewVisibility(R.id.rightPodUpdating, View.VISIBLE);
notificationSmall.setTextViewText(R.id.rightPodText, ""); notificationBig.setViewVisibility(R.id.podCaseUpdating, View.VISIBLE);
notificationSmall.setTextViewText(R.id.podCaseText, ""); notificationSmall.setViewVisibility(R.id.leftPodText, View.INVISIBLE);
notificationSmall.setViewVisibility(R.id.rightPodText, View.INVISIBLE);
notificationSmall.setViewVisibility(R.id.podCaseText, View.INVISIBLE);
notificationSmall.setViewVisibility(R.id.leftPodUpdating, View.VISIBLE);
notificationSmall.setViewVisibility(R.id.rightPodUpdating, View.VISIBLE);
notificationSmall.setViewVisibility(R.id.podCaseUpdating, View.VISIBLE);
} }
mNotifyManager.notify(1,mBuilder.build()); mNotifyManager.notify(1,mBuilder.build());
} }
sleepMs(1000); try {
Thread.sleep(1000);
} catch (InterruptedException e) { }
} }
} }
} }
private static void sleepMs(long ms){
try {
Thread.sleep(ms);
} catch (InterruptedException e) { }
}
public PodsService() { public PodsService() {
} }
@@ -167,6 +262,10 @@ public class PodsService extends Service {
private BroadcastReceiver btReceiver=null; private BroadcastReceiver btReceiver=null;
/**
* When the service is created, we register to get as many bluetooth and airpods related events as possible.
* ACL_CONNECTED and ACL_DISCONNECTED should have been enough, but you never know with android these days.
*/
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
@@ -189,20 +288,24 @@ public class PodsService extends Service {
String action = intent.getAction(); String action = intent.getAction();
if(action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)){ if(action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)){
int state= intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); int state= intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
if(state==BluetoothAdapter.STATE_OFF){ if(state==BluetoothAdapter.STATE_OFF){ //bluetooth turned off, stop scanner and remove notification
maybeConnected =false; maybeConnected =false;
stopAirPodsScanner();
recentBeacons.clear();
} }
if(state==BluetoothAdapter.STATE_ON){ if(state==BluetoothAdapter.STATE_ON){ //bluetooth turned on, start/restart scanner
startAirPodsScanner(); startAirPodsScanner();
} }
} }
if (bluetoothDevice != null && action != null && !action.isEmpty()&&checkUUID(bluetoothDevice)){ if (bluetoothDevice != null && action != null && !action.isEmpty()&&checkUUID(bluetoothDevice)){ //airpods filter
if(action.equals("android.bluetooth.device.action.ACL_CONNECTED")){ if(action.equals("android.bluetooth.device.action.ACL_CONNECTED")){ //airpods connected, show notification
if(ENABLE_LOGGING) Log.d(TAG,"ACL CONNECTED");
maybeConnected =true; maybeConnected =true;
startAirPodsScanner();
} }
if(action.equals("android.bluetooth.device.action.ACL_DISCONNECTED")){ if(action.equals("android.bluetooth.device.action.ACL_DISCONNECTED")){ //airpods disconnected, remove notification but leave the scanner going
if(ENABLE_LOGGING) Log.d(TAG,"ACL DISCONNECTED");
maybeConnected =false; maybeConnected =false;
recentBeacons.clear();
} }
} }
} }
@@ -213,7 +316,7 @@ public class PodsService extends Service {
try{ try{
registerReceiver(btReceiver,intentFilter); registerReceiver(btReceiver,intentFilter);
}catch(Throwable t){} }catch(Throwable t){}
startAirPodsScanner(); if(((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter().isEnabled())startAirPodsScanner(); //if BT is already on when the app is started, start the scanner without waiting for an event to happen
} }
private boolean checkUUID(BluetoothDevice bluetoothDevice){ private boolean checkUUID(BluetoothDevice bluetoothDevice){
@@ -243,7 +346,6 @@ public class PodsService extends Service {
n=new NotificationThread(); n=new NotificationThread();
n.start(); n.start();
} }
startAirPodsScanner();
return START_STICKY; return START_STICKY;
} }
@@ -3,7 +3,11 @@ package com.dosse.airpods;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log;
/**
* A simple starter class that starts the service when the device is booted, or after an update
*/
public class Starter extends BroadcastReceiver { public class Starter extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@@ -17,7 +17,7 @@
android:layout_height="120dp" android:layout_height="120dp"
android:src="@drawable/tick" android:src="@drawable/tick"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:id="@+id/mainTick" android:id="@+id/tick"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
/> />
<TextView <TextView
@@ -27,14 +27,21 @@
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:text="@string/main1" android:text="@string/main1"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large" android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large"
android:layout_below="@id/mainTick" android:layout_below="@id/tick"
/> />
<TextView
android:id="@+id/supportedDevices"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="60dp"
android:layout_below="@id/main1"
android:text="@string/supportedDevices" />
<TextView <TextView
android:id="@+id/main2" android:id="@+id/main2"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="80dp" android:layout_marginBottom="24dp"
android:layout_below="@id/main1" android:layout_below="@id/supportedDevices"
android:text="@string/main2" /> android:text="@string/main2" />
<Button <Button
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -44,6 +51,7 @@
android:id="@+id/mainHide" android:id="@+id/mainHide"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
/> />
</RelativeLayout> </RelativeLayout>
</RelativeLayout> </RelativeLayout>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="120dp"
android:padding="12dp"
android:gravity="center"
>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/locationDisabledText"
/>
</RelativeLayout>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="70dp"
android:padding="12dp"
android:gravity="center"
>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/locationDisabledText"
/>
</RelativeLayout>
@@ -6,6 +6,7 @@
android:gravity="center" android:gravity="center"
> >
<RelativeLayout <RelativeLayout
android:layout_width="60dp" android:layout_width="60dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -31,6 +32,16 @@
android:id="@+id/leftPodText" android:id="@+id/leftPodText"
/> />
<ProgressBar
android:id="@+id/leftPodUpdating"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="18dp"
android:layout_below="@id/leftPodImg"
android:gravity="center"
android:indeterminate="true"
/>
</RelativeLayout> </RelativeLayout>
<RelativeLayout <RelativeLayout
@@ -56,6 +67,16 @@
android:id="@+id/rightPodText" android:id="@+id/rightPodText"
/> />
<ProgressBar
android:id="@+id/rightPodUpdating"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="18dp"
android:layout_below="@id/rightPodImg"
android:gravity="center"
android:indeterminate="true"
/>
</RelativeLayout> </RelativeLayout>
<RelativeLayout <RelativeLayout
@@ -81,6 +102,16 @@
android:id="@+id/podCaseText" android:id="@+id/podCaseText"
/> />
<ProgressBar
android:id="@+id/podCaseUpdating"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="18dp"
android:layout_below="@id/podCaseImg"
android:gravity="center"
android:indeterminate="true"
/>
</RelativeLayout> </RelativeLayout>
</RelativeLayout> </RelativeLayout>
@@ -31,6 +31,15 @@
android:id="@+id/leftPodText" android:id="@+id/leftPodText"
/> />
<ProgressBar
android:id="@+id/leftPodUpdating"
style="?android:attr/progressBarStyle"
android:layout_width="18dp"
android:layout_height="30dp"
android:layout_toRightOf="@id/leftPodImg"
android:indeterminate="true"
/>
</RelativeLayout> </RelativeLayout>
<RelativeLayout <RelativeLayout
@@ -58,6 +67,15 @@
android:id="@+id/rightPodText" android:id="@+id/rightPodText"
/> />
<ProgressBar
android:id="@+id/rightPodUpdating"
style="?android:attr/progressBarStyle"
android:layout_width="18dp"
android:layout_height="30dp"
android:layout_toRightOf="@id/rightPodImg"
android:indeterminate="true"
/>
</RelativeLayout> </RelativeLayout>
<RelativeLayout <RelativeLayout
@@ -85,6 +103,15 @@
android:id="@+id/podCaseText" android:id="@+id/podCaseText"
/> />
<ProgressBar
android:id="@+id/podCaseUpdating"
style="?android:attr/progressBarStyle"
android:layout_width="18dp"
android:layout_height="30dp"
android:layout_toRightOf="@id/podCaseImg"
android:indeterminate="true"
/>
</RelativeLayout> </RelativeLayout>
@@ -7,5 +7,7 @@
<string name="main2">Puoi nascondere l\'app se vuoi</string> <string name="main2">Puoi nascondere l\'app se vuoi</string>
<string name="mainHide">Nascondi app</string> <string name="mainHide">Nascondi app</string>
<string name="hideClicked">L\'icona sarà rimossa presto</string> <string name="hideClicked">L\'icona sarà rimossa presto</string>
<string name="noBtText">Questo dispositivo non sembra avere il Bluetooth</string> <string name="noBtText">Questo dispositivo non supporta Bluetooth LE</string>
<string name="locationDisabledText">Attiva la posizione per permttere a OpenPods di leggere lo stato</string>
<string name="supportedDevices">Dispositivi supportati:\n&#8226; Apple AirPods 1st gen</string>
</resources> </resources>
+3 -1
View File
@@ -7,5 +7,7 @@
<string name="main2">You can hide this app if you want</string> <string name="main2">You can hide this app if you want</string>
<string name="mainHide">Hide app</string> <string name="mainHide">Hide app</string>
<string name="hideClicked">The icon will disappear soon</string> <string name="hideClicked">The icon will disappear soon</string>
<string name="noBtText">It looks like this device doesn\'t have Bluetooth</string> <string name="noBtText">It looks like this device doesn\'t have Bluetooth LE</string>
<string name="locationDisabledText">You need to turn on Location to allow OpenPods to read the status</string>
<string name="supportedDevices">Supported devices:\n&#8226; Apple AirPods 1st gen</string>
</resources> </resources>
+6
View File
@@ -14,11 +14,17 @@ The project is currently in Alpha.
* Respects your privacy * Respects your privacy
* Free and Open Source software * Free and Open Source software
## Supported devices
* Apple AirPods 1st gen
## Project status ## Project status
Right now, this project should be considered in alpha state. It was not extensively tested, it may not be stable or work at all on your device. Right now, this project should be considered in alpha state. It was not extensively tested, it may not be stable or work at all on your device.
**Feedback is appreciated** **Feedback is appreciated**
## Donate
[Donate with PayPal](https://www.paypal.me/sineisochronic)
## Screenshots ## Screenshots
![Screenshot: notification](https://github.com/adolfintel/OpenPods/blob/master/.github/screen1.png?raw=true) ![Screenshot: notification](https://github.com/adolfintel/OpenPods/blob/master/.github/screen1.png?raw=true)
![Screenshot: notification](https://github.com/adolfintel/OpenPods/blob/master/.github/screen2.png?raw=true) ![Screenshot: notification](https://github.com/adolfintel/OpenPods/blob/master/.github/screen2.png?raw=true)