Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,68 @@
* Powerbeats 3
* Powerbeats Pro

## Integration
This app can post broadcast intents that can be read by any apps on the same device.

To register for a broadcast receiver:

**AndroidManifest.xml**
```xml
<receiver android:name=".AirpodReceiver"
android:exported="true"
android:permission="com.dosse.airpods.permission.ACCESS_AIRPOD_INFORMATION">
<intent-filter>
<action android:name="com.dosse.airpods.status"/>
</intent-filter>
</receiver>
```

**Check and request permission**
```java
if (checkSelfPermission(context, "com.dosse.airpods.permission.ACCESS_AIRPOD_INFORMATION") == PackageManager.PERMISSION_DENIED) {
context.requestPermissions(new String[]{"com.dosse.airpods.permission.ACCESS_AIRPOD_INFORMATION"}, 201);
}
```

**Receiver**
```java
public class AirpodReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("com.dosse.airpods.status")) {
// Do something with "intent.getExtras()" here
}
}
}
```

**Register Receiver**
```java
IntentFilter airpodFilter = new IntentFilter("com.dosse.airpods.status");
AirpodReceiver airpodReceiver = new AirpodReceiver();
registerReceiver(airpodReceiver, airpodFilter);
```

**Intent Extras**

Primary action: `com.dosse.airpods.status` contains the following intent extras:

| Intent Extra | Type | Description |
|--------------------| ---- |----------------------------------------------------------------------------------------------------------------------------------------------|
| isAllDisconnected | boolean | True if airpod (including case, left pod, right pod) lost connection, otherwise false |
| model | String | Model of the airpod |
| isSingle | boolean | True if the model is single (ie. Beats Studio, Beats Solo, etc.), otherwise false |
| leftPodStatus | String | Battery percentage (with % suffix) of the left pod if it is connected, blank if disconnected. **Only applicable for non-single pod models** |
| rightPodStatus | String | Battery percentage (with % suffix) of the right pod if it is connected, blank if disconnected. **Only applicable for non-single pod models** |
| caseStatus | String | Battery percentage (with % suffix) of the charging case if it is connected, blank if disconnected. **Only applicable for non-single pod models** |
| singlePodStatus | String | Battery percentage (with % suffix) of the airpod if it is connected, blank if disconnected. **Only applicable for single pod models** |
| isLeftPodCharging | boolean | True if left pod is charging in case, otherwise false. **Only applicable for non-single pod models** |
| isRightPodCharging | boolean | True if right pod is charging in case, otherwise false. **Only applicable for non-single pod models** |
| isCaseCharging | boolean | True if airpod case is charging, otherwise false. **Only applicable for non-single pod models** |
| isSinglePodCharging | boolean | True if single pod is charging, otherwise false. **Only applicable for single pod models** |
| leftPodInEar | boolean | If left pod is detected to be in ear. **Only applicable for non-single pod models** |
| rightPodInEar | boolean | If right pod is detected to be in ear. **Only applicable for non-single pod models** |

## DO NOT REUPLOAD TO GOOGLE PLAY
**This app violates Google Play policies and is designed to break if you try to fix that unless you really know what you're doing.**<br />
Legal actions can and will be taken when uploading a compiled version to the Google Play Store that does not comply with the terms and conditions of the app's license. As a result, you may no longer be able to publish apps to the Google Play Store. This app is not intended to be uploaded to the Google Play Store. The developer is legally allowed to demand compensation.
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
android:name="android.hardware.bluetooth_le"
android:required="false" />

<permission
android:name="com.dosse.airpods.permission.ACCESS_AIRPOD_INFORMATION"
android:label="@string/read_permission_label"
android:description="@string/read_permission_desc"
android:icon="@drawable/icon_small"
android:roundIcon="@drawable/icon_small"
android:protectionLevel="dangerous" />

<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
Expand Down
70 changes: 70 additions & 0 deletions app/src/main/java/com/dosse/airpods/pods/PodsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,30 @@

import com.dosse.airpods.R;
import com.dosse.airpods.notification.NotificationThread;
import com.dosse.airpods.pods.models.RegularPods;
import com.dosse.airpods.pods.models.SinglePods;
import com.dosse.airpods.receivers.BluetoothListener;
import com.dosse.airpods.receivers.BluetoothReceiver;
import com.dosse.airpods.receivers.ScreenReceiver;
import com.dosse.airpods.utils.BroadcastParam;
import com.dosse.airpods.utils.Logger;

import java.util.Objects;

import static com.dosse.airpods.utils.BroadcastParam.ACTION_STATUS;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_IS_ALL_DISCONNECTED;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_IS_SINGLE;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_LEFT_POD_CHARGING;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_LEFT_POD_IN_EAR;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_LEFT_POD_STATUS;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_MODEL;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_POD_CASE_CHARGING;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_POD_CASE_STATUS;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_RIGHT_POD_CHARGING;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_RIGHT_POD_IN_EAR;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_RIGHT_POD_STATUS;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_SINGLE_POD_CHARGING;
import static com.dosse.airpods.utils.BroadcastParam.EXTRA_SINGLE_POD_STATUS;
import static com.dosse.airpods.pods.PodsStatusScanCallback.getScanFilters;
import static com.dosse.airpods.utils.SharedPreferencesUtils.isSavingBattery;

Expand Down Expand Up @@ -91,6 +108,11 @@ private void startAirPodsScanner() {
@Override
public void onStatus(PodsStatus newStatus) {
mStatus = newStatus;

// Sometimes after disconnecting, the scanner still lingers
if (mMaybeConnected) {
sendBroadcast();
}
}
};

Expand Down Expand Up @@ -149,6 +171,10 @@ public void onStop() {
Logger.debug("BT OFF");
mMaybeConnected = false;
stopAirPodsScanner();

// Reset status back to disconnected and send the broadcast
mStatus = PodsStatus.DISCONNECTED;
sendBroadcast();
}

@Override
Expand All @@ -168,6 +194,10 @@ public void onDisconnect(BluetoothDevice bluetoothDevice) {
// Airpods disconnected, remove notification but leave the scanner going.
Logger.debug("ACL DISCONNECTED");
mMaybeConnected = false;

// Reset status back to disconnected and send the broadcast
mStatus = PodsStatus.DISCONNECTED;
sendBroadcast();
}
}
};
Expand Down Expand Up @@ -330,4 +360,44 @@ private void unregisterScreenReceiver() {
Logger.error(t);
}
}

private void sendBroadcast() {
Intent intent = new Intent();
intent.setAction(ACTION_STATUS);

if (mStatus == PodsStatus.DISCONNECTED) {
intent.putExtra(EXTRA_IS_ALL_DISCONNECTED, true);
sendBroadcast(intent);
return;
}

intent.putExtra(EXTRA_IS_ALL_DISCONNECTED, mStatus.isAllDisconnected());
intent.putExtra(EXTRA_MODEL, mStatus.getAirpods().getModel());
intent.putExtra(EXTRA_IS_SINGLE, mStatus.getAirpods().isSingle());

if (mStatus.getAirpods().isSingle()) {
intent.putExtra(EXTRA_SINGLE_POD_STATUS,
((SinglePods) mStatus.getAirpods()).getParsedStatus());
intent.putExtra(EXTRA_SINGLE_POD_CHARGING,
((SinglePods) mStatus.getAirpods()).isCharging());
} else {
intent.putExtra(EXTRA_LEFT_POD_STATUS,
((RegularPods) mStatus.getAirpods()).getParsedStatus(RegularPods.LEFT));
intent.putExtra(EXTRA_RIGHT_POD_STATUS,
((RegularPods) mStatus.getAirpods()).getParsedStatus(RegularPods.RIGHT));
intent.putExtra(EXTRA_POD_CASE_STATUS,
((RegularPods) mStatus.getAirpods()).getParsedStatus(RegularPods.CASE));
intent.putExtra(EXTRA_LEFT_POD_IN_EAR,
((RegularPods) mStatus.getAirpods()).isInEar(RegularPods.LEFT));
intent.putExtra(EXTRA_RIGHT_POD_IN_EAR,
((RegularPods) mStatus.getAirpods()).isInEar(RegularPods.RIGHT));
intent.putExtra(EXTRA_LEFT_POD_CHARGING,
((RegularPods) mStatus.getAirpods()).isCharging(RegularPods.LEFT));
intent.putExtra(EXTRA_RIGHT_POD_CHARGING,
((RegularPods) mStatus.getAirpods()).isCharging(RegularPods.RIGHT));
intent.putExtra(EXTRA_POD_CASE_CHARGING,
((RegularPods) mStatus.getAirpods()).isCharging(RegularPods.CASE));
}
sendBroadcast(intent, BroadcastParam.PERMISSION_ACCESS_AIRPOD_INFORMATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ public String getParsedStatus(int pos) {
return pods[pos].parseStatus();
}

public boolean isInEar(int pos) {
return pods[pos].isInEar();
}

public boolean isCharging(int pos) {
return pods[pos].isCharging();
}

public int getInEarVisibility(int pos) {
return pods[pos].inEarVisibility();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public String getParsedStatus() {
return pod.parseStatus();
}

public boolean isCharging() {
return pod.isCharging();
}

public int getBatImgVisibility() {
return pod.batImgVisibility();
}
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/dosse/airpods/utils/BroadcastParam.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dosse.airpods.utils;

public class BroadcastParam {
public static final String PERMISSION_ACCESS_AIRPOD_INFORMATION = "com.dosse.airpods.permission.ACCESS_AIRPOD_INFORMATION";
public static final String ACTION_STATUS = "com.dosse.airpods.status";
public static final String EXTRA_IS_ALL_DISCONNECTED = "isAllDisconnected";
public static final String EXTRA_MODEL = "model";
public static final String EXTRA_IS_SINGLE = "isSingle";
public static final String EXTRA_LEFT_POD_STATUS = "leftPodStatus";
public static final String EXTRA_RIGHT_POD_STATUS = "rightPodStatus";
public static final String EXTRA_POD_CASE_STATUS = "caseStatus";
public static final String EXTRA_LEFT_POD_IN_EAR = "leftPodInEar";
public static final String EXTRA_RIGHT_POD_IN_EAR = "rightPodInEar";
public static final String EXTRA_SINGLE_POD_STATUS = "singlePodStatus";
public static final String EXTRA_LEFT_POD_CHARGING = "isLeftPodCharging";
public static final String EXTRA_RIGHT_POD_CHARGING = "isRightPodCharging";
public static final String EXTRA_POD_CASE_CHARGING = "isCaseCharging";
public static final String EXTRA_SINGLE_POD_CHARGING = "isSinglePodCharging";
}
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/icon_small.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="25dp"
android:height="25dp">
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/icon"
android:tint="@color/dgrey"/>
</item>
</layer-list>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
<string name="restart" tools:ignore="MissingTranslation">Restart Service</string>
<string name="restart_desc" tools:ignore="MissingTranslation">Do this if the notification is not showing</string>

<string name="read_permission_label">Read Airpod Information</string>
<string name="read_permission_desc">access information about your AirPods, including battery status, connection status, and device settings</string>

<string name="about">About</string>
<string name="about_dev">Developed by Federico Dossena and Itai Levin</string>
<string name="fdroid" translatable="false">F-Droid</string>
Expand Down