24 Commits
Author SHA1 Message Date
dosse91 d089b350ea Release 1.5.1 2021-03-26 07:36:46 +01:00
Federico Dossena 8167e84c7d Merge pull request #112 from Reflexe/bugfix/107-remote-views-memory-leak
Bugfix: Recreate RemoteViews on every iteration
2021-02-12 19:59:07 +01:00
Federico Dossena 7b23d3e1e7 Merge pull request #113 from Gijuno/master
Added Korean Strings.xml
2021-01-26 11:35:41 +01:00
Gijuno 158dfe2382 Added Korean Strings.xml
Added Korean Strings.xml
2021-01-26 18:14:35 +09:00
Gijuno 71c5f61638 Delete OpenPods/app/src/main/res/values-ko directory 2021-01-26 18:14:02 +09:00
Gijuno 067b39aaa7 Add files via upload 2021-01-26 18:12:49 +09:00
Shmuel H faef81399d Bugfix: Recreate RemoteViews on every iteration
Fix a memory leak caused by trying to update the same RemoteViews. Instead of trying to update them, create them from scratch on every iteration.

 The memory leak on RemoteViews caused the Parcel to get very big and therefore undeliverable by the Samsung NotificationService.

 See https://stackoverflow.com/a/4484693/6797552

Fixes #107
2021-01-10 23:53:50 +02:00
Electric f00ff42980 Change target api to 29 for possible better performance and compatibility. 2021-01-05 15:06:54 +02:00
Electric 195fb9520a Change ACCESS_COARSE_LOCATION to ACCESS_FINE_LOCATION 2020-12-07 15:22:34 +02:00
Electric 6da1ab1d7d Added documentation to in ear detection 2020-12-07 15:19:28 +02:00
Electric 67b8ea654f Change service exported to false 2020-12-07 14:20:42 +02:00
Electric b345a91630 Update PodsService.java 2020-11-30 15:30:18 +02:00
Electric 69c2577c12 Added in ear detection 2020-11-30 15:21:01 +02:00
Electric cd8e855428 Move location RemoteViews to a single instance. 2020-11-16 16:18:35 +02:00
Electric 53377a60c8 Cleaned up some code 2020-11-16 15:59:45 +02:00
Electric 4fc0b39aa9 Possible fix for #106 2020-11-16 08:19:41 +02:00
Electric c2a42f2015 Updated gradle files 2020-11-16 08:18:13 +02:00
Electric ae5a1ab119 Fixed appid 2020-11-03 10:57:24 +02:00
Electric 50fa300c04 Update build tools 2020-11-03 10:14:50 +02:00
Federico Dossena 7d8d286350 Merge pull request #103 from hadrienbecle/patch-1
Fixed tiny typos in French localization
2020-10-04 12:38:43 +02:00
Hadrien Bècle 65cef15357 Fixed tiny typos in French localization 2020-10-04 12:36:17 +02:00
Electric ea1743a3fa Updated isFlipped 2020-08-24 15:50:47 +03:00
Electric 783ab3c018 Cleaned up and reformatted hex decoding functions. 2020-08-24 14:50:56 +03:00
Electric 9c86da7115 Updated build tools 2020-08-24 14:47:22 +03:00
19 changed files with 232 additions and 224 deletions
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
</component>
</project>
+1
View File
@@ -15,6 +15,7 @@
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>
+1 -1
View File
@@ -5,7 +5,7 @@
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+5 -5
View File
@@ -2,14 +2,15 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion '30.0.0'
buildToolsVersion '30.0.2'
defaultConfig {
applicationId "com.dosse.airpods"
minSdkVersion 23
//noinspection OldTargetApi
targetSdkVersion 29
versionCode 15
versionName '1.5'
versionCode 16
versionName '1.5.1'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -33,7 +34,6 @@ android {
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.preference:preference:1.1.1"
}
+3 -4
View File
@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.dosse.airpods">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -66,8 +65,8 @@
<service
android:name=".PodsService"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedService" />
android:exported="false"
android:foregroundServiceType="location" />
</application>
@@ -29,7 +29,7 @@ public class IntroActivity extends AppCompatActivity {
// Allow button clicked, ask for permissions
findViewById(R.id.allowBtn).setOnClickListener(view -> {
requestPermissions(new String[] {Manifest.permission.ACCESS_COARSE_LOCATION}, 1); // Location (for BLE)
requestPermissions(new String[] {Manifest.permission.ACCESS_FINE_LOCATION}, 1); // Location (for BLE)
// Run in background
try {
if (!Objects.requireNonNull(getSystemService(PowerManager.class)).isIgnoringBatteryOptimizations(getPackageName())) {
@@ -57,7 +57,7 @@ public class IntroActivity extends AppCompatActivity {
} catch (Throwable ignored) {
}
if (ContextCompat.checkSelfPermission(IntroActivity.this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
if (ContextCompat.checkSelfPermission(IntroActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED)
ok = false;
if (ok) {
@@ -43,7 +43,7 @@ public class MainActivity extends AppCompatActivity {
} catch (Throwable ignored) {
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED)
ok = false;
if (ok) {
@@ -45,11 +45,14 @@ import java.util.Objects;
* - 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 void OpenPodsDebugLog (String msg) {
if (BuildConfig.DEBUG) Log.d(TAG, msg); // 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 boolean inEarL = false, inEarR = false;
private static final String MODEL_AIRPODS_NORMAL = "airpods12", MODEL_AIRPODS_PRO = "airpodspro";
private static String model = MODEL_AIRPODS_NORMAL;
@@ -73,20 +76,23 @@ public class PodsService extends Service {
* 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 12th and 13th characters in the string represent the charge of the left and right pods. Under unknown circumstances[1], 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
* - The 11th character in the string represents the in-ear detection status. Bit 1 is the left pod; Bit 3 is the right pod.
* - The 7th character in the string represents the AirPods model (E=AirPods pro)
* <p>
* After decoding a beacon, the status is written to leftStatus, rightStatus, caseStatus, chargeL, chargeR, chargeCase so that the NotificationThread can use the information
* After decoding a beacon, the status is written to leftStatus, rightStatus, caseStatus, chargeL, chargeR, chargeCase, inEarL, inEarR so that the NotificationThread can use the information
* <p>
* Notes:
* 1) - isFlipped set by bit 1 of 10th character in the string; seems to be related to in-ear detection;
*/
private static ArrayList<ScanResult> recentBeacons = new ArrayList<>();
private static final ArrayList<ScanResult> 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");
OpenPodsDebugLog("START SCANNER");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
BluetoothManager btManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
@@ -116,98 +122,73 @@ public class PodsService extends Service {
else
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);
}
btScanner.startScan(filters, settings, new ScanCallback() {
@Override
public void onBatchScanResults (List<ScanResult> scanResults) {
for (ScanResult result : scanResults)
onScanResult(-1, result);
super.onBatchScanResults(scanResults);
}
@Override
public void onScanResult (int callbackType, ScanResult result) {
try {
byte[] data = Objects.requireNonNull(result.getScanRecord()).getManufacturerSpecificData(76);
@Override
public void onScanResult (int callbackType, ScanResult result) {
try {
byte[] data = Objects.requireNonNull(result.getScanRecord()).getManufacturerSpecificData(76);
if (data == null || data.length != 27)
return;
if (data == null || data.length != 27)
return;
recentBeacons.add(result);
recentBeacons.add(result);
if (ENABLE_LOGGING) {
Log.d(TAG, "" + result.getRssi() + "db");
Log.d(TAG, decodeHex(data));
}
OpenPodsDebugLog("" + result.getRssi() + "db");
OpenPodsDebugLog(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)));
boolean flip = isFlipped(a);
String leftAirpodStr; // Left airpod (0-10 batt; 15=disconnected)
String rightAirpodStr; // Right airpod (0-10 batt; 15=disconnected)
if (flip) {
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);
if (flip) {
chargeL = (chargeStatus & 0b00000010) != 0;
chargeR = (chargeStatus & 0b00000001) != 0;
} else {
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);
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)));
boolean flip = isFlipped(a);
leftStatus = Integer.parseInt("" + a.charAt(flip ? 12 : 13), 16); // Left airpod (0-10 batt; 15=disconnected)
rightStatus = Integer.parseInt("" + a.charAt(flip ? 13 : 12), 16); // Right airpod (0-10 batt; 15=disconnected)
caseStatus = Integer.parseInt("" + a.charAt(15), 16); // Case (0-10 batt; 15=disconnected)
int chargeStatus = Integer.parseInt("" + a.charAt(14), 16); // Charge status (bit 0=left; bit 1=right; bit 2=case)
chargeL = (chargeStatus & (flip ? 0b00000010 : 0b00000001)) != 0;
chargeR = (chargeStatus & (flip ? 0b00000001 : 0b00000010)) != 0;
chargeCase = (chargeStatus & 0b00000100) != 0;
int inEarStatus = Integer.parseInt("" + a.charAt(11), 16); // InEar status (bit 1=left; bit 3=right)
inEarL = (inEarStatus & (flip ? 0b00001000 : 0b00000010)) != 0;
inEarR = (inEarStatus & (flip ? 0b00000010 : 0b00001000)) != 0;
model = (a.charAt(7) == 'E') ? MODEL_AIRPODS_PRO : MODEL_AIRPODS_NORMAL; // Detect if these are AirPods Pro or regular ones
lastSeenConnected = System.currentTimeMillis();
} catch (Throwable t) {
OpenPodsDebugLog("" + t);
}
}
});
} catch (Throwable t) {
if (ENABLE_LOGGING)
Log.d(TAG, "" + t);
OpenPodsDebugLog("" + t);
}
}
@@ -230,8 +211,7 @@ public class PodsService extends Service {
private void stopAirPodsScanner () {
try {
if (btScanner != null) {
if (ENABLE_LOGGING)
Log.d(TAG, "STOP SCANNER");
OpenPodsDebugLog("STOP SCANNER");
btScanner.stopScan(new ScanCallback() {
@Override
@@ -246,22 +226,17 @@ public class PodsService extends Service {
}
}
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];
StringBuilder ret = new StringBuilder();
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];
}
for (byte b : bArr)
ret.append(String.format("%02X", b));
return new String(ret);
return ret.toString();
}
private boolean isFlipped (String str) {
return (Integer.toString(Integer.parseInt("" + str.charAt(10), 16) + 0x10, 2)).charAt(3) == '0';
return (Integer.parseInt("" + str.charAt(10), 16) & 0x02) == 0;
}
/**
@@ -294,7 +269,7 @@ public class PodsService extends Service {
}
}
private NotificationManager mNotifyManager;
private final NotificationManager mNotifyManager;
@SuppressWarnings("WeakerAccess")
public NotificationThread () {
@@ -315,10 +290,7 @@ public class PodsService extends Service {
boolean notificationShowing = false;
String compat = getPackageManager().getInstallerPackageName(getPackageName());
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);
RemoteViews[] notificationLocation = new RemoteViews[] {new RemoteViews(getPackageName(), R.layout.location_disabled_big), new RemoteViews(getPackageName(), R.layout.location_disabled_small)};
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(PodsService.this, TAG);
mBuilder.setShowWhen(false);
@@ -327,18 +299,18 @@ public class PodsService extends Service {
mBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
for (; ; ) {
RemoteViews[] notificationArr = new RemoteViews[] {new RemoteViews(getPackageName(), R.layout.status_big), new RemoteViews(getPackageName(), R.layout.status_small)};
/*&&System.currentTimeMillis()-lastSeenConnected<TIMEOUT_CONNECTED*/
if (maybeConnected && !(leftStatus == 15 && rightStatus == 15 && caseStatus == 15)) {
if (!notificationShowing) {
if (ENABLE_LOGGING)
Log.d(TAG, "Creating notification");
OpenPodsDebugLog("Creating notification");
notificationShowing = true;
mNotifyManager.notify(1, mBuilder.build());
}
} else {
if (notificationShowing) {
if (ENABLE_LOGGING)
Log.d(TAG, "Removing notification");
OpenPodsDebugLog("Removing notification");
notificationShowing = false;
continue;
}
@@ -347,95 +319,66 @@ public class PodsService extends Service {
// Apparently this restriction was removed in android Q
if (isLocationEnabled() || Build.VERSION.SDK_INT >= 29) {
mBuilder.setCustomContentView(notificationSmall);
mBuilder.setCustomBigContentView(notificationBig);
mBuilder.setCustomContentView(notificationArr[1]);
mBuilder.setCustomBigContentView(notificationArr[0]);
} else {
mBuilder.setCustomContentView(locationDisabledSmall);
mBuilder.setCustomBigContentView(locationDisabledBig);
mBuilder.setCustomContentView(notificationLocation[1]);
mBuilder.setCustomBigContentView(notificationLocation[0]);
}
if (notificationShowing) {
if (ENABLE_LOGGING)
Log.d(TAG, "Left: " + leftStatus + (chargeL ? "+" : "") + " Right: " + rightStatus + (chargeR ? "+" : "") + " Case: " + caseStatus + (chargeCase ? "+" : "") + " Model: " + model);
OpenPodsDebugLog("Left: " + leftStatus + (chargeL ? "+" : "") + (inEarL ? "$" : "") + " Right: " + rightStatus + (chargeR ? "+" : "") + (inEarR ? "$" : "") + " Case: " + caseStatus + (chargeCase ? "+" : "") + " Model: " + model);
if (model.equals(MODEL_AIRPODS_NORMAL)) {
notificationBig.setImageViewResource(R.id.leftPodImg, leftStatus <= 10 ? R.drawable.pod : R.drawable.pod_disconnected);
notificationBig.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.pod : R.drawable.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.pod : R.drawable.pod_disconnected);
notificationSmall.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.pod : R.drawable.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)) {
notificationBig.setImageViewResource(R.id.leftPodImg, leftStatus <= 10 ? R.drawable.podpro : R.drawable.podpro_disconnected);
notificationBig.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.podpro : R.drawable.podpro_disconnected);
notificationBig.setImageViewResource(R.id.podCaseImg, caseStatus <= 10 ? R.drawable.podpro_case : R.drawable.podpro_case_disconnected);
notificationSmall.setImageViewResource(R.id.leftPodImg, leftStatus <= 10 ? R.drawable.podpro : R.drawable.podpro_disconnected);
notificationSmall.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.podpro : R.drawable.podpro_disconnected);
notificationSmall.setImageViewResource(R.id.podCaseImg, caseStatus <= 10 ? R.drawable.podpro_case : R.drawable.podpro_case_disconnected);
if (model.equals(MODEL_AIRPODS_NORMAL)) for (RemoteViews notification : notificationArr) {
notification.setImageViewResource(R.id.leftPodImg, leftStatus <= 10 ? R.drawable.pod : R.drawable.pod_disconnected);
notification.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.pod : R.drawable.pod_disconnected);
notification.setImageViewResource(R.id.podCaseImg, caseStatus <= 10 ? R.drawable.pod_case : R.drawable.pod_case_disconnected);
}
else if (model.equals(MODEL_AIRPODS_PRO)) for (RemoteViews notification : notificationArr) {
notification.setImageViewResource(R.id.leftPodImg, leftStatus <= 10 ? R.drawable.podpro : R.drawable.podpro_disconnected);
notification.setImageViewResource(R.id.rightPodImg, rightStatus <= 10 ? R.drawable.podpro : R.drawable.podpro_disconnected);
notification.setImageViewResource(R.id.podCaseImg, caseStatus <= 10 ? R.drawable.podpro_case : R.drawable.podpro_case_disconnected);
}
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);
if (System.currentTimeMillis() - lastSeenConnected < TIMEOUT_CONNECTED) for (RemoteViews notification : notificationArr) {
notification.setViewVisibility(R.id.leftPodText, View.VISIBLE);
notification.setViewVisibility(R.id.rightPodText, View.VISIBLE);
notification.setViewVisibility(R.id.podCaseText, View.VISIBLE);
notification.setViewVisibility(R.id.leftPodUpdating, View.INVISIBLE);
notification.setViewVisibility(R.id.rightPodUpdating, View.INVISIBLE);
notification.setViewVisibility(R.id.podCaseUpdating, View.INVISIBLE);
String podText_Left = (leftStatus == 10) ? "100%" : ((leftStatus < 10) ? ((leftStatus * 10 + 5) + "%") : "");
String podText_Right = (rightStatus == 10) ? "100%" : ((rightStatus < 10) ? ((rightStatus * 10 + 5) + "%") : "");
String podText_Case = (caseStatus == 10) ? "100%" : ((caseStatus < 10) ? ((caseStatus * 10 + 5) + "%") : "");
notificationBig.setTextViewText(R.id.leftPodText, podText_Left);
notificationBig.setTextViewText(R.id.rightPodText, podText_Right);
notificationBig.setTextViewText(R.id.podCaseText, podText_Case);
notification.setTextViewText(R.id.leftPodText, podText_Left);
notification.setTextViewText(R.id.rightPodText, podText_Right);
notification.setTextViewText(R.id.podCaseText, podText_Case);
notificationSmall.setTextViewText(R.id.leftPodText, podText_Left);
notificationSmall.setTextViewText(R.id.rightPodText, podText_Right);
notificationSmall.setTextViewText(R.id.podCaseText, podText_Case);
notification.setImageViewResource(R.id.leftBatImg, chargeL ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notification.setImageViewResource(R.id.rightBatImg, chargeR ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notification.setImageViewResource(R.id.caseBatImg, chargeCase ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notificationBig.setImageViewResource(R.id.leftBatImg, chargeL ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notificationBig.setImageViewResource(R.id.rightBatImg, chargeR ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notificationBig.setImageViewResource(R.id.caseBatImg, chargeCase ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notification.setViewVisibility(R.id.leftBatImg, ((chargeL && leftStatus <= 10) || (leftStatus <= 1) ? View.VISIBLE : View.GONE));
notification.setViewVisibility(R.id.rightBatImg, ((chargeR && rightStatus <= 10) || (rightStatus <= 1) ? View.VISIBLE : View.GONE));
notification.setViewVisibility(R.id.caseBatImg, ((chargeCase && caseStatus <= 10) || (caseStatus <= 1) ? View.VISIBLE : View.GONE));
notificationSmall.setImageViewResource(R.id.leftBatImg, chargeL ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notificationSmall.setImageViewResource(R.id.rightBatImg, chargeR ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notificationSmall.setImageViewResource(R.id.caseBatImg, chargeCase ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp);
notificationBig.setViewVisibility(R.id.leftBatImg, ((chargeL && leftStatus <= 10) || (leftStatus <= 1) ? View.VISIBLE : View.GONE));
notificationBig.setViewVisibility(R.id.rightBatImg, ((chargeR && rightStatus <= 10) || (rightStatus <= 1) ? View.VISIBLE : View.GONE));
notificationBig.setViewVisibility(R.id.caseBatImg, ((chargeCase && caseStatus <= 10) || (caseStatus <= 1) ? View.VISIBLE : View.GONE));
notificationSmall.setViewVisibility(R.id.leftBatImg, ((chargeL && leftStatus <= 10) || (leftStatus <= 1) ? View.VISIBLE : View.GONE));
notificationSmall.setViewVisibility(R.id.rightBatImg, ((chargeR && rightStatus <= 10) || (rightStatus <= 1) ? View.VISIBLE : View.GONE));
notificationSmall.setViewVisibility(R.id.caseBatImg, ((chargeCase && caseStatus <= 10) || (caseStatus <= 1) ? View.VISIBLE : View.GONE));
} else {
notificationBig.setViewVisibility(R.id.leftPodText, View.INVISIBLE);
notificationBig.setViewVisibility(R.id.rightPodText, View.INVISIBLE);
notificationBig.setViewVisibility(R.id.podCaseText, View.INVISIBLE);
notificationBig.setViewVisibility(R.id.leftBatImg, View.GONE);
notificationBig.setViewVisibility(R.id.rightBatImg, View.GONE);
notificationBig.setViewVisibility(R.id.caseBatImg, View.GONE);
notificationBig.setViewVisibility(R.id.leftPodUpdating, View.VISIBLE);
notificationBig.setViewVisibility(R.id.rightPodUpdating, View.VISIBLE);
notificationBig.setViewVisibility(R.id.podCaseUpdating, View.VISIBLE);
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.leftBatImg, View.GONE);
notificationSmall.setViewVisibility(R.id.rightBatImg, View.GONE);
notificationSmall.setViewVisibility(R.id.caseBatImg, View.GONE);
notificationSmall.setViewVisibility(R.id.leftPodUpdating, View.VISIBLE);
notificationSmall.setViewVisibility(R.id.rightPodUpdating, View.VISIBLE);
notificationSmall.setViewVisibility(R.id.podCaseUpdating, View.VISIBLE);
notification.setViewVisibility(R.id.leftInEarImg, inEarL ? View.VISIBLE : View.INVISIBLE);
notification.setViewVisibility(R.id.rightInEarImg, inEarR ? View.VISIBLE : View.INVISIBLE);
}
else for (RemoteViews notification : notificationArr) {
notification.setViewVisibility(R.id.leftPodText, View.INVISIBLE);
notification.setViewVisibility(R.id.rightPodText, View.INVISIBLE);
notification.setViewVisibility(R.id.podCaseText, View.INVISIBLE);
notification.setViewVisibility(R.id.leftBatImg, View.GONE);
notification.setViewVisibility(R.id.rightBatImg, View.GONE);
notification.setViewVisibility(R.id.caseBatImg, View.GONE);
notification.setViewVisibility(R.id.leftPodUpdating, View.VISIBLE);
notification.setViewVisibility(R.id.rightPodUpdating, View.VISIBLE);
notification.setViewVisibility(R.id.podCaseUpdating, View.VISIBLE);
notification.setViewVisibility(R.id.leftInEarImg, View.INVISIBLE);
notification.setViewVisibility(R.id.rightInEarImg, View.INVISIBLE);
}
try {
@@ -448,6 +391,7 @@ public class PodsService extends Service {
if ((compat == null ? 0 : (compat.hashCode()) ^ 0x43700437) == 0x82e89606) return;
try {
//noinspection BusyWait
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
@@ -503,8 +447,7 @@ public class PodsService extends Service {
// Bluetooth turned off, stop scanner and remove notification.
if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) {
if (ENABLE_LOGGING)
Log.d(TAG, "BT OFF");
OpenPodsDebugLog("BT OFF");
maybeConnected = false;
stopAirPodsScanner();
recentBeacons.clear();
@@ -512,8 +455,7 @@ public class PodsService extends Service {
// Bluetooth turned on, start/restart scanner.
if (state == BluetoothAdapter.STATE_ON) {
if (ENABLE_LOGGING)
Log.d(TAG, "BT ON");
OpenPodsDebugLog("BT ON");
startAirPodsScanner();
}
}
@@ -522,15 +464,13 @@ public class PodsService extends Service {
if (bluetoothDevice != null && !action.isEmpty() && checkUUID(bluetoothDevice)) {
// Airpods connected, show notification.
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
if (ENABLE_LOGGING)
Log.d(TAG, "ACL CONNECTED");
OpenPodsDebugLog("ACL CONNECTED");
maybeConnected = true;
}
// Airpods disconnected, remove notification but leave the scanner going.
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) || action.equals(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED)) {
if (ENABLE_LOGGING)
Log.d(TAG, "ACL DISCONNECTED");
OpenPodsDebugLog("ACL DISCONNECTED");
maybeConnected = false;
recentBeacons.clear();
}
@@ -550,15 +490,13 @@ public class PodsService extends Service {
@Override
public void onServiceConnected (int i, BluetoothProfile bluetoothProfile) {
if (i == BluetoothProfile.HEADSET) {
if (ENABLE_LOGGING)
Log.d(TAG, "BT PROXY SERVICE CONNECTED");
OpenPodsDebugLog("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");
OpenPodsDebugLog("BT PROXY: AIRPODS ALREADY CONNECTED");
maybeConnected = true;
break;
}
@@ -568,8 +506,7 @@ public class PodsService extends Service {
@Override
public void onServiceDisconnected (int i) {
if (i == BluetoothProfile.HEADSET) {
if (ENABLE_LOGGING)
Log.d(TAG, "BT PROXY SERVICE DISCONNECTED ");
OpenPodsDebugLog("BT PROXY SERVICE DISCONNECTED ");
maybeConnected = false;
}
}
@@ -594,12 +531,10 @@ public class PodsService extends Service {
@Override
public void onReceive (Context context, Intent intent) {
if (Objects.equals(intent.getAction(), Intent.ACTION_SCREEN_OFF)) {
if (ENABLE_LOGGING)
Log.d(TAG, "SCREEN OFF");
OpenPodsDebugLog("SCREEN OFF");
stopAirPodsScanner();
} else if (Objects.equals(intent.getAction(), Intent.ACTION_SCREEN_ON)) {
if (ENABLE_LOGGING)
Log.d(TAG, "SCREEN ON");
OpenPodsDebugLog("SCREEN ON");
BluetoothAdapter ba = ((BluetoothManager)Objects.requireNonNull(getSystemService(Context.BLUETOOTH_SERVICE))).getAdapter();
if (ba.isEnabled())
startAirPodsScanner();
@@ -2,7 +2,6 @@ package com.dosse.airpods;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/textColorPrimary"
android:pathData="M15,20c-0.3,0 -0.6,-0.1 -0.8,-0.1c-0.7,-0.4 -1.2,-0.9 -1.7,-2.4c-0.5,-1.6 -1.5,-2.3 -2.4,-3c-0.8,-0.6 -1.6,-1.2 -2.3,-2.5C7.3,11 7,9.9 7,9c0,-2.8 2.2,-5 5,-5s5,2.2 5,5h2c0,-3.9 -3.1,-7 -7,-7S5,5.1 5,9c0,1.3 0.4,2.6 1.1,3.9C7,14.5 8,15.4 8.9,16c0.8,0.6 1.4,1.1 1.7,2c0.6,1.8 1.4,2.8 2.7,3.5c0.5,0.2 1.1,0.4 1.6,0.4c2.2,0 4,-1.8 4,-4h-2C17,19.1 16.1,20 15,20zM9.5,9c0,1.4 1.1,2.5 2.5,2.5s2.5,-1.1 2.5,-2.5S13.4,6.5 12,6.5S9.5,7.6 9.5,9z" />
</vector>
@@ -23,6 +23,15 @@
android:layout_marginBottom="6dp"
android:src="@drawable/pod" />
<ImageView
android:id="@+id/leftInEarImg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/leftPodImg"
android:layout_alignParentLeft="true"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_inear_24dp" />
<TextView
android:id="@+id/leftPodText"
android:layout_width="wrap_content"
@@ -67,6 +76,16 @@
android:scaleX="-1"
android:src="@drawable/pod" />
<ImageView
android:id="@+id/rightInEarImg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/rightPodImg"
android:layout_alignParentRight="true"
android:layout_marginBottom="8dp"
android:scaleX="-1"
android:src="@drawable/ic_inear_24dp" />
<TextView
android:id="@+id/rightPodText"
android:layout_width="wrap_content"
@@ -11,8 +11,8 @@
<string name="miui_warning_continue">Continue</string>
<string name="noBtText">Il semble que cet appareil ne dispose pas de Bluetooth LE.</string>
<string name="locationDisabledText">Vous devez activer la localisation pour permettre aux OpenPod de lire l\'état de vous AirPods.</string>
<string name="supportedDevicesTitle"><u>Appareils pris en charge:</u></string>
<string name="locationDisabledText">Vous devez activer la localisation pour permettre aux OpenPod de lire l\'état de vos AirPods.</string>
<string name="supportedDevicesTitle"><u>Appareils pris en charge :</u></string>
<string name="supportedDevicesItems">&#8226; Apple AirPods 1re génération\n&#8226; Apple AirPods 2e génération\n&#8226; Apple AirPods Pro</string>
<string name="batterySaver">Économiseur de batterie (non recommandé)</string>
@@ -22,8 +22,8 @@
<string name="hideClicked">L\'icône disparaîtra bientôt.</string>
<string name="about">À propos</string>
<string name="about_dev">Developpé par Federico Dossena</string>
<string name="about_dev">Développé par Federico Dossena</string>
<string name="website">Site web</string>
<string name="github">Github</string>
<string name="donate">Faire un don</string>
</resources>
</resources>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" translatable="false">OpenPods</string>
<string name="intro1">이 앱을 통해 Android 기기에서 에어팟의 상태를 확인할 수 있습니다.</string>
<string name="intro2">작동하려면 Bluetooth LE(위치)를 사용할 수 있는 권한과 백그라운드에서 실행할 수 있는 권한이 필요합니다.</string>
<string name="introAllow">허용</string>
<string name="main_description">앱에 에어팟이 연결되면 알림이 표시됩니다.</string>
<string name="hide_message">이 앱을 숨길 수 있습니다.</string>
<string name="settings">설정</string>
<string name="miui_warning">MIUI 발견됨</string>
<string name="miui_warning_desc">이 장치에서 실행 중인 ROM에 알려진 호환성 문제가 있습니다. 계속할 수는 있지만 OpenPod가 제대로 작동하지 않을 수 있습니다.</string>
<string name="miui_warning_continue">계속</string>
<string name="noBtText">이 장치에는 Bluetooth LE가 없는 것 같습니다.</string>
<string name="locationDisabledText">OpenPod에서 상태를 읽을 수 있도록 하려면 위치를 허용해야 합니다.</string>
<string name="supportedDevicesTitle"><u>지원하는 기기:</u></string>
<string name="supportedDevicesItems">&#8226; 애플 에어팟 1세대\n&#8226; 애플 에어팟 2세대\n&#8226; 애플 에어팟 프로</string>
<string name="batterySaver">배터리 관리모드 (추천하지 않음)</string>
<string name="batterySaver_desc">Bluetooth에서 배터리를 많이 사용하는 경우 이 옵션을 활성화하세요.</string>
<string name="hide">앱 숨기기</string>
<string name="hide_desc">실행 프로그램에서 이 앱 숨기기</string>
<string name="hide_dialog" tools:ignore="MissingTranslation">정말 계속하시겠습니까?</string>
<string name="hide_dialog_desc" tools:ignore="MissingTranslation">정말 이 앱을 숨기시겠습니까? 앱을 다시 설치해야 기능을 해제할 수 있습니다.</string>
<string name="hide_dialog_button" tools:ignore="MissingTranslation">숨기기</string>
<string name="hideClicked">아이콘이 곧 사라집니다</string>
<string name="about">정보</string>
<string name="about_dev">개발자 : Federico Dossena</string>
<string name="website">웹사이트</string>
<string name="github">Github</string>
<string name="donate">후원하기</string>
<string name="fdroid" translatable="false">F-Droid</string>
</resources>
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
+1 -1
View File
@@ -7,7 +7,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.android.tools.build:gradle:4.1.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+2 -2
View File
@@ -1,6 +1,6 @@
#Mon Jun 08 20:04:20 IDT 2020
#Mon Nov 30 15:19:04 IST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip