Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35a9d96398 | ||
|
|
298ce85990 | ||
|
|
18158d70b2 | ||
|
|
0f74a0607e | ||
|
|
3bd3ec74e7 | ||
|
|
5c39fb37f7 | ||
|
|
243a947110 | ||
|
|
e299e5e346 | ||
|
|
aaab15ee87 | ||
|
|
5fb7c3a39a | ||
|
|
6c65cb36f9 | ||
|
|
24963be8bd | ||
|
|
5a82530057 | ||
|
|
d6d8b3a586 | ||
|
|
ae4f6ea1af | ||
|
|
f771481a2d | ||
|
|
44580f2f2a | ||
|
|
c043ade2d9 | ||
|
|
4138d440b2 | ||
|
|
dcd886c84b | ||
|
|
9248bdcf96 | ||
|
|
613f116b28 | ||
|
|
f06206f125 | ||
|
|
f421e0febf | ||
|
|
34be06663d | ||
|
|
09af4a76e1 | ||
|
|
e233938851 | ||
|
|
4ff0535a4e | ||
|
|
7edc1a01ef | ||
|
|
05451b5e88 | ||
|
|
d4459ea3a3 | ||
|
|
8262db31d8 | ||
|
|
9e6ef70fab | ||
|
|
21db852661 | ||
|
|
c11e261bba | ||
|
|
cff007f161 | ||
|
|
18be725cf3 | ||
|
|
df90f89785 | ||
|
|
d08c5a2c22 | ||
|
|
2b66a73874 | ||
|
|
af673910b0 | ||
|
|
2e0d0347c1 | ||
|
|
5e55d0bdc0 | ||
|
|
d864b62efd |
|
Before Width: | Height: | Size: 51 KiB |
@@ -1,29 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<code_scheme name="Project" version="173">
|
|
||||||
<Objective-C-extensions>
|
|
||||||
<file>
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
|
|
||||||
</file>
|
|
||||||
<class>
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
|
|
||||||
</class>
|
|
||||||
<extensions>
|
|
||||||
<pair source="cpp" header="h" fileNamingConvention="NONE" />
|
|
||||||
<pair source="c" header="h" fileNamingConvention="NONE" />
|
|
||||||
</extensions>
|
|
||||||
</Objective-C-extensions>
|
|
||||||
</code_scheme>
|
|
||||||
</component>
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||||
|
</project>
|
||||||
@@ -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>
|
||||||
@@ -6,8 +6,8 @@ android {
|
|||||||
applicationId "com.dosse.airpods"
|
applicationId "com.dosse.airpods"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 1
|
versionCode 8
|
||||||
versionName "1.0"
|
versionName '0.8'
|
||||||
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"
|
||||||
@@ -44,7 +45,6 @@
|
|||||||
<activity android:name=".MainActivity">
|
<activity android:name=".MainActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,79 @@
|
|||||||
package com.dosse.airpods;
|
package com.dosse.airpods;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.bluetooth.BluetoothAdapter;
|
import android.bluetooth.BluetoothAdapter;
|
||||||
import android.bluetooth.BluetoothDevice;
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothHeadset;
|
||||||
import android.bluetooth.BluetoothManager;
|
import android.bluetooth.BluetoothManager;
|
||||||
|
import android.bluetooth.BluetoothProfile;
|
||||||
import android.bluetooth.le.BluetoothLeScanner;
|
import android.bluetooth.le.BluetoothLeScanner;
|
||||||
import android.bluetooth.le.ScanCallback;
|
import android.bluetooth.le.ScanCallback;
|
||||||
|
import android.bluetooth.le.ScanFilter;
|
||||||
|
import android.bluetooth.le.ScanFilter.Builder;
|
||||||
import android.bluetooth.le.ScanResult;
|
import android.bluetooth.le.ScanResult;
|
||||||
|
import android.bluetooth.le.ScanSettings;
|
||||||
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.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.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;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* - 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
|
||||||
|
* - 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);
|
||||||
@@ -37,14 +81,40 @@ public class PodsService extends Service {
|
|||||||
btScanner = btAdapter.getBluetoothLeScanner();
|
btScanner = btAdapter.getBluetoothLeScanner();
|
||||||
if (btAdapter == null) throw new Exception("No BT");
|
if (btAdapter == null) throw new Exception("No BT");
|
||||||
if (!btAdapter.isEnabled()) throw new Exception("BT Off");
|
if (!btAdapter.isEnabled()) throw new Exception("BT Off");
|
||||||
btScanner.startScan(new ScanCallback() {
|
|
||||||
|
List<ScanFilter> filters = getScanFilters();
|
||||||
|
ScanSettings settings = new ScanSettings.Builder().setScanMode(2).setReportDelay(2).build();
|
||||||
|
|
||||||
|
btScanner.startScan(
|
||||||
|
filters,
|
||||||
|
settings,
|
||||||
|
new ScanCallback() {
|
||||||
|
@Override
|
||||||
|
public void onBatchScanResults(List<ScanResult> scanResults) {
|
||||||
|
for (ScanResult result : scanResults) onScanResult(-1, result);
|
||||||
|
super.onBatchScanResults(scanResults);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
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;
|
if(ENABLE_LOGGING) Log.d(TAG,""+result.getRssi()+"db");
|
||||||
|
if(ENABLE_LOGGING) 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;
|
||||||
|
if(result.getRssi()<-60) return;
|
||||||
|
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 +135,41 @@ 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 List<ScanFilter> getScanFilters() {
|
||||||
|
byte[] manufacturerData = new byte[27];
|
||||||
|
byte[] manufacturerDataMask = new byte[27];
|
||||||
|
|
||||||
|
manufacturerData[0] = 7;
|
||||||
|
manufacturerData[1] = 25;
|
||||||
|
|
||||||
|
manufacturerDataMask[0] = -1;
|
||||||
|
manufacturerDataMask[1] = -1;
|
||||||
|
|
||||||
|
Builder builder = new Builder();
|
||||||
|
builder.setManufacturerData(76, manufacturerData, manufacturerDataMask);
|
||||||
|
return Collections.singletonList(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +185,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 =false;
|
||||||
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
|
||||||
@@ -102,60 +217,80 @@ public class PodsService extends Service {
|
|||||||
channel.enableVibration(false);
|
channel.enableVibration(false);
|
||||||
channel.enableLights(false);
|
channel.enableLights(false);
|
||||||
channel.setShowBadge(true);
|
channel.setShowBadge(true);
|
||||||
|
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||||
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.mipmap.notification_icon);
|
||||||
for(;;){
|
for(;;){
|
||||||
if(maybeConnected &&!(leftStatus==15&&rightStatus==15&&caseStatus==15)){
|
if(maybeConnected &&!(leftStatus==15&&rightStatus==15&&caseStatus==15)/*&&System.currentTimeMillis()-lastSeenConnected<TIMEOUT_CONNECTED*/){
|
||||||
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;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
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.setTextViewText(R.id.leftPodText, (leftStatus <= 10 ? (leftStatus * 10) + "%" : "") + ((chargeL && leftStatus < 10) ? "+" : ""));
|
notificationBig.setViewVisibility(R.id.leftPodText, View.VISIBLE);
|
||||||
notificationBig.setTextViewText(R.id.rightPodText, (rightStatus <= 10 ? (rightStatus * 10) + "%" : "") + ((chargeR && rightStatus < 10) ? "+" : ""));
|
notificationBig.setViewVisibility(R.id.rightPodText, View.VISIBLE);
|
||||||
notificationBig.setTextViewText(R.id.podCaseText, (caseStatus <= 10 ? (caseStatus * 10) + "%" : "") + (chargeCase ? "+" : ""));
|
notificationBig.setViewVisibility(R.id.podCaseText, View.VISIBLE);
|
||||||
notificationSmall.setTextViewText(R.id.leftPodText, (leftStatus <= 10 ? (leftStatus * 10) + "%" : "") + (chargeL ? "+" : ""));
|
notificationBig.setViewVisibility(R.id.leftPodUpdating, View.INVISIBLE);
|
||||||
notificationSmall.setTextViewText(R.id.rightPodText, (rightStatus <= 10 ? (rightStatus * 10) + "%" : "") + (chargeR ? "+" : ""));
|
notificationBig.setViewVisibility(R.id.rightPodUpdating, View.INVISIBLE);
|
||||||
notificationSmall.setTextViewText(R.id.podCaseText, (caseStatus <= 10 ? (caseStatus * 10) + "%" : "") + (chargeCase ? "+" : ""));
|
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?"100%":leftStatus<10?((leftStatus+1)*10+"%"):"") + ((chargeL && leftStatus < 10) ? "+" : ""));
|
||||||
|
notificationBig.setTextViewText(R.id.rightPodText, (rightStatus==10?"100%":rightStatus<10?((rightStatus+1)*10+"%"):"") + ((chargeR && rightStatus < 10) ? "+" : ""));
|
||||||
|
notificationBig.setTextViewText(R.id.podCaseText, (caseStatus==10?"100%":caseStatus<10?((caseStatus+1)*10+"%"):"") + ((chargeCase && caseStatus < 10) ? "+" : ""));
|
||||||
|
notificationSmall.setTextViewText(R.id.leftPodText, (leftStatus==10?"100%":leftStatus<10?((leftStatus+1)*10+"%"):"") + ((chargeL && leftStatus < 10) ? "+" : ""));
|
||||||
|
notificationSmall.setTextViewText(R.id.rightPodText, (rightStatus==10?"100%":rightStatus<10?((rightStatus+1)*10+"%"):"") + ((chargeR && rightStatus < 10) ? "+" : ""));
|
||||||
|
notificationSmall.setTextViewText(R.id.podCaseText, (caseStatus==10?"100%":caseStatus<10?((caseStatus+1)*10+"%"):"") + ((chargeCase && caseStatus < 10) ? "+" : ""));
|
||||||
}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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sleepMs(long ms){
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(ms);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException e) { }
|
} catch (InterruptedException e) { }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public PodsService() {
|
public PodsService() {
|
||||||
}
|
}
|
||||||
@@ -167,6 +302,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 +328,26 @@ 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||state==BluetoothAdapter.STATE_TURNING_OFF){ //bluetooth turned off, stop scanner and remove notification
|
||||||
|
if(ENABLE_LOGGING) Log.d(TAG,"BT OFF");
|
||||||
maybeConnected =false;
|
maybeConnected =false;
|
||||||
|
stopAirPodsScanner();
|
||||||
|
recentBeacons.clear();
|
||||||
}
|
}
|
||||||
if(state==BluetoothAdapter.STATE_ON){
|
if(state==BluetoothAdapter.STATE_ON){ //bluetooth turned on, start/restart scanner
|
||||||
|
if(ENABLE_LOGGING) Log.d(TAG,"BT ON");
|
||||||
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(BluetoothDevice.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(BluetoothDevice.ACTION_ACL_DISCONNECTED)||action.equals(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED)){ //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 +358,34 @@ public class PodsService extends Service {
|
|||||||
try{
|
try{
|
||||||
registerReceiver(btReceiver,intentFilter);
|
registerReceiver(btReceiver,intentFilter);
|
||||||
}catch(Throwable t){}
|
}catch(Throwable t){}
|
||||||
startAirPodsScanner();
|
//this BT Profile Proxy allows us to know if airpods are already connected when the app is started. It also fires an event when BT is turned off, in case the BroadcastReceiver doesn't do its job
|
||||||
|
BluetoothAdapter ba=((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
|
||||||
|
ba.getProfileProxy(getApplicationContext(), new BluetoothProfile.ServiceListener() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) {
|
||||||
|
if(i==BluetoothProfile.HEADSET){
|
||||||
|
if(ENABLE_LOGGING) Log.d(TAG,"BT PROXY SERVICE CONNECTED");
|
||||||
|
BluetoothHeadset h=(BluetoothHeadset)bluetoothProfile;
|
||||||
|
for(BluetoothDevice d:h.getConnectedDevices()){
|
||||||
|
if(checkUUID(d)){
|
||||||
|
if(ENABLE_LOGGING) Log.d(TAG,"BT PROXY: AIRPODS ALREADY CONNECTED");
|
||||||
|
maybeConnected=true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(int i) {
|
||||||
|
if(i==BluetoothProfile.HEADSET){
|
||||||
|
if(ENABLE_LOGGING) Log.d(TAG,"BT PROXY SERVICE DISCONNECTED ");
|
||||||
|
maybeConnected=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},BluetoothProfile.HEADSET);
|
||||||
|
if(ba.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 +415,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>
|
||||||
@@ -4,8 +4,10 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
android:background="@color/white"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -29,6 +31,17 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="100%+"
|
android:text="100%+"
|
||||||
android:id="@+id/leftPodText"
|
android:id="@+id/leftPodText"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -54,6 +67,17 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="100%+"
|
android:text="100%+"
|
||||||
android:id="@+id/rightPodText"
|
android:id="@+id/rightPodText"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -79,6 +103,17 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="100%+"
|
android:text="100%+"
|
||||||
android:id="@+id/podCaseText"
|
android:id="@+id/podCaseText"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
android:background="@color/white"
|
||||||
>
|
>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
@@ -29,6 +30,16 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:text="100%+"
|
android:text="100%+"
|
||||||
android:id="@+id/leftPodText"
|
android:id="@+id/leftPodText"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -56,6 +67,16 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:text="100%+"
|
android:text="100%+"
|
||||||
android:id="@+id/rightPodText"
|
android:id="@+id/rightPodText"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -83,6 +104,16 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:text="100%+"
|
android:text="100%+"
|
||||||
android:id="@+id/podCaseText"
|
android:id="@+id/podCaseText"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 658 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 684 B |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="intro1">Diese App erlaubt es dir, den Status deiner AirPods auf einem beliebigen Android-Gerät zu überprüfen</string>
|
||||||
|
<string name="intro2">Um zu funktionieren, benötigen wir die Berechtigungen um Bluetooth LE (Standort) zu benutzen und im Hintergrund zu laufen</string>
|
||||||
|
<string name="introAllow">Erlauben</string>
|
||||||
|
<string name="main1">Die App zeigt eine Benachrichtigung wenn deine AirPods verbunden sind</string>
|
||||||
|
<string name="main2">Du kannst die App jetzt verstecken wenn du möchtest</string>
|
||||||
|
<string name="mainHide">App verstecken</string>
|
||||||
|
<string name="hideClicked">Das Icon wird bald verschwinden</string>
|
||||||
|
<string name="noBtText">Es sieht so aus als hätte dieses Gerät kein Bluetooth LE</string>
|
||||||
|
<string name="locationDisabledText">Du musst Standortinformationen aktivieren, damit OpenPods den Status deiner AirPods auslesen kann</string>
|
||||||
|
<string name="supportedDevices">Unterstützte Geräte:\n• Apple AirPods 1st gen\n• Apple AirPods 2nd gen</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="intro1">Esta aplicación te permite ver el estado de tus AirPods en cualquier dispositivo Android</string>
|
||||||
|
<string name="intro2">Para operar, necesitamos permiso para usar Bluetooth LE (Localización) y permiso para ejecutarnos en el fondo</string>
|
||||||
|
<string name="introAllow">Permitir</string>
|
||||||
|
<string name="main1">Esta aplicación mostrará una notificación cuando tus AirPods estén conectados</string>
|
||||||
|
<string name="main2">Puedes ocultar la app si gustas</string>
|
||||||
|
<string name="mainHide">Ocultar app</string>
|
||||||
|
<string name="hideClicked">El ícono desaparecerá pronto</string>
|
||||||
|
<string name="noBtText">Parece que este dispositivo no tiene Bluetooth LE</string>
|
||||||
|
<string name="locationDisabledText">Necesitas encender Localización para permitir que OpenPods lea el estado</string>
|
||||||
|
<string name="supportedDevices">Dispositivos compatibles:\n• Apple AirPods 1st gen\n• Apple AirPods 2nd gen</string>
|
||||||
|
</resources>
|
||||||
@@ -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• Apple AirPods 1st gen\n• Apple AirPods 2nd gen</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="intro1">此应用可以让您在任何安卓设备上查看 AirPods 的状态</string>
|
||||||
|
<string name="intro2">我们需要蓝牙、定位以及在后台运行的权限来正常运作</string>
|
||||||
|
<string name="introAllow">允许</string>
|
||||||
|
<string name="main1">当您的 AirPods 接入时,我们会在通知栏显示通知</string>
|
||||||
|
<string name="main2">您可以隐藏此应用</string>
|
||||||
|
<string name="mainHide">隐藏</string>
|
||||||
|
<string name="hideClicked">桌面图标很快就会隐藏掉</string>
|
||||||
|
<string name="noBtText">看起来您的设备并不支持蓝牙</string>
|
||||||
|
<string name="locationDisabledText">您需要开启定位来让 OpenPods 读取状态</string>
|
||||||
|
<string name="supportedDevices">支持的设备:\n• Apple AirPods 1st gen\n• Apple AirPods 2nd gen</string>
|
||||||
|
</resources>
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
<color name="colorPrimary">#448aff</color>
|
<color name="colorPrimary">#448aff</color>
|
||||||
<color name="colorPrimaryDark">#0d47a1</color>
|
<color name="colorPrimaryDark">#0d47a1</color>
|
||||||
<color name="colorAccent">#448aff</color>
|
<color name="colorAccent">#448aff</color>
|
||||||
|
<color name="white">#ffffff</color>
|
||||||
|
<color name="black">#000000</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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• Apple AirPods 1st gen\n• Apple AirPods 2nd gen</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
classpath 'com.android.tools.build:gradle:3.4.0'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#Tue Mar 12 08:48:48 CET 2019
|
#Tue May 14 16:46:59 CEST 2019
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||

|

|
||||||
|
|
||||||
# OpenPods
|
# OpenPods
|
||||||
The Free and Open Source app for monitoring your AirPods on Android
|
The Free and Open Source app for monitoring your AirPods on Android
|
||||||
@@ -6,24 +6,31 @@ The Free and Open Source app for monitoring your AirPods on Android
|
|||||||
## Download
|
## Download
|
||||||
The project is currently in Alpha.
|
The project is currently in Alpha.
|
||||||
|
|
||||||
|
[Download from F-Droid](https://f-droid.org/repository/browse/?fdid=com.dosse.airpods)
|
||||||
[Download APK](https://downloads.fdossena.com/geth.php?r=openpods-apk)
|
[Download APK](https://downloads.fdossena.com/geth.php?r=openpods-apk)
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Shows a notification with AirPods status when they are connected
|
* Shows a notification with AirPods status when they are connected
|
||||||
* Respects your privacy
|
* Respects your privacy
|
||||||
* Free and Open Source software
|
* Free and Open Source software
|
||||||
|
|
||||||
|
## Supported devices
|
||||||
|
* Apple AirPods 1st gen
|
||||||
|
* Apple AirPods 2nd 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
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## License
|
## License
|
||||||
Copyright (C) 2019 Federico Dossena
|
Copyright (C) 2019 Federico Dossena
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 815 KiB After Width: | Height: | Size: 815 KiB |
|
Before Width: | Height: | Size: 994 KiB After Width: | Height: | Size: 994 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 63 KiB |