Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cd800875f | ||
|
|
f693b9a5c5 | ||
|
|
5ab3b13350 | ||
|
|
8c462cc32c | ||
|
|
695dea99bd | ||
|
|
e0110d9264 | ||
|
|
7c21e2d464 | ||
|
|
50e6833c8d | ||
|
|
6a00d8ce24 | ||
|
|
93d52efc27 | ||
|
|
299aad0b7e | ||
|
|
3268a71f9b | ||
|
|
35aeae2e2b | ||
|
|
fbd643cd5f | ||
|
|
4b42f13a02 | ||
|
|
fbba6cd54f | ||
|
|
30fa4443e9 | ||
|
|
2fef18973b | ||
|
|
13d2945d3b | ||
|
|
f768564a43 | ||
|
|
60de5515e3 | ||
|
|
e52c254a45 | ||
|
|
131f470344 | ||
|
|
702c8116d1 | ||
|
|
56b7df7b32 | ||
|
|
414085698f | ||
|
|
4590e46b64 | ||
|
|
d03dc6b374 | ||
|
|
ec3e32e3f3 | ||
|
|
f16d02786e | ||
|
|
4ef334661a | ||
|
|
05f10b00e6 | ||
|
|
888c6e7d0d | ||
|
|
019ef80ec9 | ||
|
|
4abb9e5d6b | ||
|
|
b15dcfbf9d | ||
|
|
92f64ae2dd | ||
|
|
a39dbc3785 | ||
|
|
ee7e75b5e8 | ||
|
|
b939674a62 | ||
|
|
ade3d0a481 | ||
|
|
70a05a1848 | ||
|
|
6803ef0691 | ||
|
|
71f08a4098 | ||
|
|
17afa55203 | ||
|
|
96ffaf1d90 | ||
|
|
047b9b373c |
@@ -8,7 +8,7 @@
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0">
|
||||
<img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg" alt="License: GPLv3">
|
||||
</a>
|
||||
<img alt="Minimum SDK" src="https://img.shields.io/badge/API-23%2B-green">
|
||||
<img alt="Minimum SDK" src="https://img.shields.io/badge/API-26%2B-green">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://f-droid.org/packages/com.dosse.airpods">
|
||||
@@ -47,6 +47,8 @@
|
||||
* Apple AirPods 2nd gen
|
||||
* Apple AirPods 3rd gen
|
||||
* Apple AirPods Pro
|
||||
* Apple AirPods Pro 2
|
||||
* Apple AirPods Pro 3
|
||||
* Apple AirPods Max
|
||||
* Beats X
|
||||
* Beats Flex
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
apply plugin: 'com.android.application'
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion '32.0.0'
|
||||
namespace 'com.dosse.airpods'
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.dosse.airpods"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 31
|
||||
versionCode 20
|
||||
versionName '1.8'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
minSdk 26
|
||||
targetSdk 36
|
||||
versionCode 24
|
||||
versionName '1.11'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -21,18 +21,16 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.androidx.preference
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 20,
|
||||
"versionName": "1.8",
|
||||
"versionCode": 21,
|
||||
"versionName": "1.9",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.dosse.airpods">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH_SCAN"
|
||||
android:usesPermissionFlags="neverForLocation"
|
||||
tools:targetApi="s" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
@@ -23,7 +29,8 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="DataExtractionRules">
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
@@ -47,13 +54,10 @@
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".ui.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.NoBTActivity"
|
||||
android:label="@string/app_name" />
|
||||
<activity android:name=".ui.NoBTActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.IntroActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<receiver
|
||||
|
||||
@@ -8,36 +8,41 @@ import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.PodsStatus;
|
||||
import com.dosse.airpods.pods.data.IPods;
|
||||
import com.dosse.airpods.pods.data.RegularPods;
|
||||
import com.dosse.airpods.pods.data.SinglePods;
|
||||
import com.dosse.airpods.pods.models.IPods;
|
||||
import com.dosse.airpods.pods.models.RegularPods;
|
||||
import com.dosse.airpods.pods.models.SinglePods;
|
||||
|
||||
public class NotificationBuilder {
|
||||
public static final String TAG = "AirPods";
|
||||
public static final long TIMEOUT_CONNECTED = 30000;
|
||||
public static final int NOTIFICATION_ID = 1;
|
||||
|
||||
private final RemoteViews[] notificationArr;
|
||||
private final RemoteViews[] mRemoteViews;
|
||||
private final NotificationCompat.Builder mBuilder;
|
||||
|
||||
public NotificationBuilder (Context context) {
|
||||
notificationArr = new RemoteViews[] {new RemoteViews(context.getPackageName(), R.layout.status_big), new RemoteViews(context.getPackageName(), R.layout.status_small)};
|
||||
public NotificationBuilder(Context context) {
|
||||
mRemoteViews = new RemoteViews[] {
|
||||
new RemoteViews(context.getPackageName(), R.layout.status_big),
|
||||
new RemoteViews(context.getPackageName(), R.layout.status_small)
|
||||
};
|
||||
|
||||
mBuilder = new NotificationCompat.Builder(context, TAG);
|
||||
mBuilder.setShowWhen(false);
|
||||
mBuilder.setOngoing(true);
|
||||
mBuilder.setSmallIcon(R.mipmap.notification_icon);
|
||||
mBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
||||
mBuilder.setCustomContentView(notificationArr[1]);
|
||||
mBuilder.setCustomBigContentView(notificationArr[0]);
|
||||
mBuilder.setCustomContentView(mRemoteViews[1]);
|
||||
mBuilder.setCustomBigContentView(mRemoteViews[0]);
|
||||
mBuilder.setStyle(new NotificationCompat.DecoratedCustomViewStyle()); //Make the notification extendable issue #165
|
||||
mBuilder.setCategory(NotificationCompat.CATEGORY_SERVICE); //show notification on android 12 and above #144 #143
|
||||
}
|
||||
|
||||
public Notification build (PodsStatus status) {
|
||||
public Notification build(PodsStatus status) {
|
||||
|
||||
IPods airpods = status.getAirpods();
|
||||
boolean single = airpods.isSingle();
|
||||
|
||||
for (RemoteViews notification : notificationArr) {
|
||||
for (RemoteViews notification : mRemoteViews) {
|
||||
if (!single) {
|
||||
notification.setImageViewResource(R.id.leftPodImg, ((RegularPods)airpods).getLeftDrawable());
|
||||
notification.setImageViewResource(R.id.rightPodImg, ((RegularPods)airpods).getRightDrawable());
|
||||
@@ -50,7 +55,7 @@ public class NotificationBuilder {
|
||||
notification.setViewVisibility(R.id.rightPod, single ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
if (isFreshStatus(status)) for (RemoteViews notification : notificationArr) {
|
||||
if (isFreshStatus(status)) for (RemoteViews notification : mRemoteViews) {
|
||||
notification.setViewVisibility(R.id.leftPodText, View.VISIBLE);
|
||||
notification.setViewVisibility(R.id.rightPodText, View.VISIBLE);
|
||||
notification.setViewVisibility(R.id.podCaseText, View.VISIBLE);
|
||||
@@ -82,8 +87,7 @@ public class NotificationBuilder {
|
||||
notification.setImageViewResource(R.id.caseBatImg, singlePods.getBatImgSrcId());
|
||||
notification.setViewVisibility(R.id.caseBatImg, singlePods.getBatImgVisibility());
|
||||
}
|
||||
}
|
||||
else for (RemoteViews notification : notificationArr) {
|
||||
} else for (RemoteViews notification : mRemoteViews) {
|
||||
notification.setViewVisibility(R.id.leftPodText, View.INVISIBLE);
|
||||
notification.setViewVisibility(R.id.rightPodText, View.INVISIBLE);
|
||||
notification.setViewVisibility(R.id.podCaseText, View.INVISIBLE);
|
||||
@@ -100,8 +104,7 @@ public class NotificationBuilder {
|
||||
return mBuilder.build();
|
||||
}
|
||||
|
||||
private boolean isFreshStatus (PodsStatus status) {
|
||||
private boolean isFreshStatus(PodsStatus status) {
|
||||
return System.currentTimeMillis() - status.getTimestamp() < TIMEOUT_CONNECTED;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import com.dosse.airpods.pods.PodsStatus;
|
||||
import com.dosse.airpods.utils.Logger;
|
||||
@@ -14,11 +13,11 @@ import static com.dosse.airpods.notification.NotificationBuilder.TAG;
|
||||
|
||||
/**
|
||||
* The following class is a thread that manages the notification while your AirPods are connected.
|
||||
*
|
||||
* <p>
|
||||
* 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)
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*/
|
||||
@@ -27,30 +26,29 @@ public abstract class NotificationThread extends Thread {
|
||||
private static final long SLEEP_TIMEOUT = 1000;
|
||||
|
||||
private final String compat;
|
||||
private final NotificationBuilder builder;
|
||||
private final NotificationManager mNotifyManager;
|
||||
private final NotificationBuilder mBuilder;
|
||||
private final NotificationManager mNotificationManager;
|
||||
|
||||
public abstract boolean isConnected ();
|
||||
public abstract boolean isConnected();
|
||||
|
||||
public abstract PodsStatus getStatus ();
|
||||
public abstract PodsStatus getStatus();
|
||||
|
||||
public NotificationThread (Context context) {
|
||||
public NotificationThread(Context context) {
|
||||
compat = context.getPackageManager().getInstallerPackageName(context.getPackageName());
|
||||
mNotifyManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
// On Oreo (API27) and newer, create a notification channel.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(false);
|
||||
channel.enableLights(false);
|
||||
channel.setShowBadge(false);
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
mNotifyManager.createNotificationChannel(channel);
|
||||
}
|
||||
builder = new NotificationBuilder(context);
|
||||
mNotificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(false);
|
||||
channel.enableLights(false);
|
||||
channel.setShowBadge(false);
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
mBuilder = new NotificationBuilder(context);
|
||||
}
|
||||
|
||||
public void run () {
|
||||
public void run() {
|
||||
boolean notificationShowing = false;
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
@@ -62,14 +60,15 @@ public abstract class NotificationThread extends Thread {
|
||||
notificationShowing = true;
|
||||
}
|
||||
Logger.debug(status.getAirpods().parseStatusForLogger());
|
||||
mNotifyManager.notify(NOTIFICATION_ID, builder.build(status));
|
||||
mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build(status));
|
||||
} else {
|
||||
if (notificationShowing) {
|
||||
Logger.debug("Removing notification");
|
||||
notificationShowing = false;
|
||||
continue;
|
||||
}
|
||||
mNotifyManager.cancel(NOTIFICATION_ID);
|
||||
|
||||
mNotificationManager.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
if ((compat == null ? 0 : (compat.hashCode()) ^ 0x43700437) == 0x82e89606) return;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
|
||||
public class Pod {
|
||||
|
||||
public static final int DISCONNECTED_STATUS = 15;
|
||||
public static final int MAX_CONNECTED_STATUS = 10;
|
||||
public static final int LOW_BATTERY_STATUS = 1;
|
||||
@@ -14,50 +13,49 @@ public class Pod {
|
||||
private final boolean charging;
|
||||
private final boolean inEar;
|
||||
|
||||
public Pod (int status, boolean charging, boolean inEar) {
|
||||
public Pod(int status, boolean charging, boolean inEar) {
|
||||
this.status = status;
|
||||
this.charging = charging;
|
||||
this.inEar = inEar;
|
||||
}
|
||||
|
||||
public int getStatus () {
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String parseStatus () {
|
||||
public String parseStatus() {
|
||||
return (status == MAX_CONNECTED_STATUS) ? "100%" : ((status < MAX_CONNECTED_STATUS) ? ((status * 10 + 5) + "%") : "");
|
||||
}
|
||||
|
||||
public boolean isCharging () {
|
||||
public boolean isCharging() {
|
||||
return charging;
|
||||
}
|
||||
|
||||
public boolean isInEar () {
|
||||
public boolean isInEar() {
|
||||
return inEar;
|
||||
}
|
||||
|
||||
public boolean isConnected () {
|
||||
public boolean isConnected() {
|
||||
return status <= MAX_CONNECTED_STATUS;
|
||||
}
|
||||
|
||||
public boolean isDisconnected () {
|
||||
public boolean isDisconnected() {
|
||||
return status == DISCONNECTED_STATUS;
|
||||
}
|
||||
|
||||
public boolean isLowBattery () {
|
||||
public boolean isLowBattery() {
|
||||
return status <= LOW_BATTERY_STATUS;
|
||||
}
|
||||
|
||||
public int inEarVisibility () {
|
||||
public int inEarVisibility() {
|
||||
return inEar ? View.VISIBLE : View.INVISIBLE;
|
||||
}
|
||||
|
||||
public int batImgVisibility () {
|
||||
public int batImgVisibility() {
|
||||
return (charging && isConnected() || isLowBattery()) ? View.VISIBLE : View.GONE;
|
||||
}
|
||||
|
||||
public int batImgSrcId () {
|
||||
public int batImgSrcId() {
|
||||
return charging ? R.drawable.ic_battery_charging_full_green_24dp : R.drawable.ic_battery_alert_red_24dp;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.dosse.airpods.pods;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
@@ -14,11 +15,9 @@ import android.bluetooth.le.ScanSettings;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelUuid;
|
||||
import android.provider.Settings;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.notification.NotificationThread;
|
||||
@@ -39,16 +38,15 @@ import static com.dosse.airpods.utils.SharedPreferencesUtils.isSavingBattery;
|
||||
* - Display the notification with the status
|
||||
*/
|
||||
public class PodsService extends Service {
|
||||
private BluetoothLeScanner mBluetoothScanner;
|
||||
private PodsStatus mStatus = PodsStatus.DISCONNECTED;
|
||||
|
||||
private BluetoothLeScanner btScanner;
|
||||
private PodsStatus status = PodsStatus.DISCONNECTED;
|
||||
private static NotificationThread mNotificationThread = null;
|
||||
private static boolean mMaybeConnected = false;
|
||||
|
||||
private static NotificationThread n = null;
|
||||
private static boolean maybeConnected = false;
|
||||
|
||||
private BroadcastReceiver btReceiver = null;
|
||||
private BroadcastReceiver screenReceiver = null;
|
||||
private PodsStatusScanCallback scanCallback = null;
|
||||
private BroadcastReceiver mBluetoothReceiver = null;
|
||||
private BroadcastReceiver mScreenReceiver = null;
|
||||
private PodsStatusScanCallback mScanCallback = null;
|
||||
|
||||
/**
|
||||
* The following method (startAirPodsScanner) creates a bluetooth LE scanner.
|
||||
@@ -56,10 +54,11 @@ public class PodsService extends Service {
|
||||
* - Check that the beacon comes from something that looks like a pair of AirPods
|
||||
* - Make sure that it is YOUR pair of AirPods
|
||||
* - Decode the beacon to get the status
|
||||
*
|
||||
* <p>
|
||||
* After decoding a beacon, the status is written to PodsStatus so that the NotificationThread can use the information
|
||||
*/
|
||||
private void startAirPodsScanner () {
|
||||
@SuppressLint("MissingPermission")
|
||||
private void startAirPodsScanner() {
|
||||
try {
|
||||
Logger.debug("START SCANNER");
|
||||
|
||||
@@ -71,10 +70,9 @@ public class PodsService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
//if (batterySaver && btScanner != null && scanCallback != null) {
|
||||
if (btScanner != null && scanCallback != null) {
|
||||
btScanner.stopScan(scanCallback);
|
||||
scanCallback = null;
|
||||
if (mBluetoothScanner != null && mScanCallback != null) {
|
||||
mBluetoothScanner.stopScan(mScanCallback);
|
||||
mScanCallback = null;
|
||||
}
|
||||
|
||||
if (!btAdapter.isEnabled()) {
|
||||
@@ -82,45 +80,46 @@ public class PodsService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
btScanner = btAdapter.getBluetoothLeScanner();
|
||||
mBluetoothScanner = btAdapter.getBluetoothLeScanner();
|
||||
|
||||
ScanSettings scanSettings = new ScanSettings.Builder()
|
||||
.setScanMode(isSavingBattery(getApplicationContext()) ? ScanSettings.SCAN_MODE_LOW_POWER : ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||
.setReportDelay(1) // DON'T USE 0
|
||||
.build();
|
||||
|
||||
scanCallback = new PodsStatusScanCallback() {
|
||||
mScanCallback = new PodsStatusScanCallback() {
|
||||
@Override
|
||||
public void onStatus (PodsStatus newStatus) {
|
||||
status = newStatus;
|
||||
public void onStatus(PodsStatus newStatus) {
|
||||
mStatus = newStatus;
|
||||
}
|
||||
};
|
||||
|
||||
btScanner.startScan(getScanFilters(), scanSettings, scanCallback);
|
||||
mBluetoothScanner.startScan(getScanFilters(), scanSettings, mScanCallback);
|
||||
} catch (Throwable t) {
|
||||
Logger.error(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAirPodsScanner () {
|
||||
@SuppressLint("MissingPermission")
|
||||
private void stopAirPodsScanner() {
|
||||
try {
|
||||
if (btScanner != null && scanCallback != null) {
|
||||
if (mBluetoothScanner != null && mScanCallback != null) {
|
||||
Logger.debug("STOP SCANNER");
|
||||
|
||||
btScanner.stopScan(scanCallback);
|
||||
scanCallback = null;
|
||||
mBluetoothScanner.stopScan(mScanCallback);
|
||||
mScanCallback = null;
|
||||
}
|
||||
status = PodsStatus.DISCONNECTED;
|
||||
mStatus = PodsStatus.DISCONNECTED;
|
||||
} catch (Throwable t) {
|
||||
Logger.error(t);
|
||||
}
|
||||
}
|
||||
|
||||
public PodsService () {
|
||||
public PodsService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind (Intent intent) {
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -128,54 +127,53 @@ public class PodsService extends Service {
|
||||
* 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.
|
||||
*/
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
@Override
|
||||
public void onCreate () {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
startForeground(101, createBackgroundNotification());
|
||||
startForeground(101, createBackgroundNotification());
|
||||
|
||||
unregisterBtReceiver();
|
||||
|
||||
btReceiver = new BluetoothReceiver() {
|
||||
mBluetoothReceiver = new BluetoothReceiver() {
|
||||
@Override
|
||||
public void onStart () {
|
||||
public void onStart() {
|
||||
// Bluetooth turned on, start/restart scanner.
|
||||
Logger.debug("BT ON");
|
||||
startAirPodsScanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop () {
|
||||
public void onStop() {
|
||||
// Bluetooth turned off, stop scanner and remove notification.
|
||||
Logger.debug("BT OFF");
|
||||
maybeConnected = false;
|
||||
mMaybeConnected = false;
|
||||
stopAirPodsScanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect (BluetoothDevice bluetoothDevice) {
|
||||
public void onConnect(BluetoothDevice bluetoothDevice) {
|
||||
// Airpods filter
|
||||
if (checkUUID(bluetoothDevice)) {
|
||||
// Airpods connected, show notification.
|
||||
Logger.debug("ACL CONNECTED");
|
||||
maybeConnected = true;
|
||||
mMaybeConnected = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect (BluetoothDevice bluetoothDevice) {
|
||||
public void onDisconnect(BluetoothDevice bluetoothDevice) {
|
||||
// Airpods filter
|
||||
if (checkUUID(bluetoothDevice)) {
|
||||
// Airpods disconnected, remove notification but leave the scanner going.
|
||||
Logger.debug("ACL DISCONNECTED");
|
||||
maybeConnected = false;
|
||||
mMaybeConnected = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
registerReceiver(btReceiver, BluetoothReceiver.buildFilter());
|
||||
registerReceiver(mBluetoothReceiver, BluetoothReceiver.buildFilter());
|
||||
} catch (Throwable t) {
|
||||
Logger.error(t);
|
||||
}
|
||||
@@ -185,12 +183,12 @@ public class PodsService extends Service {
|
||||
BluetoothAdapter ba = ((BluetoothManager)Objects.requireNonNull(getSystemService(Context.BLUETOOTH_SERVICE))).getAdapter();
|
||||
ba.getProfileProxy(getApplicationContext(), new BluetoothListener() {
|
||||
@Override
|
||||
public boolean onConnect (BluetoothDevice device) {
|
||||
public boolean onConnect(BluetoothDevice device) {
|
||||
Logger.debug("BT PROXY SERVICE CONNECTED ");
|
||||
|
||||
if (checkUUID(device)) {
|
||||
Logger.debug("BT PROXY: AIRPODS ALREADY CONNECTED");
|
||||
maybeConnected = true;
|
||||
mMaybeConnected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -198,88 +196,95 @@ public class PodsService extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect () {
|
||||
public void onDisconnect() {
|
||||
Logger.debug("BT PROXY SERVICE DISCONNECTED ");
|
||||
maybeConnected = false;
|
||||
mMaybeConnected = 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
|
||||
// If BT is already on when the app is started, start the scanner without waiting for an event to happen
|
||||
if (ba.isEnabled()) {
|
||||
startAirPodsScanner();
|
||||
}
|
||||
|
||||
// Screen on/off listener to suspend scanning when the screen is off, to save battery
|
||||
unregisterScreenReceiver();
|
||||
|
||||
if (isSavingBattery(getApplicationContext())) {
|
||||
screenReceiver = new ScreenReceiver() {
|
||||
mScreenReceiver = new ScreenReceiver() {
|
||||
@Override
|
||||
public void onStart () {
|
||||
public void onStart() {
|
||||
Logger.debug("SCREEN ON");
|
||||
startAirPodsScanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop () {
|
||||
public void onStop() {
|
||||
Logger.debug("SCREEN OFF");
|
||||
stopAirPodsScanner();
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
registerReceiver(screenReceiver, ScreenReceiver.buildFilter());
|
||||
registerReceiver(mScreenReceiver, ScreenReceiver.buildFilter());
|
||||
} catch (Throwable t) {
|
||||
Logger.error(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checkUUID (BluetoothDevice bluetoothDevice) {
|
||||
@SuppressLint("MissingPermission")
|
||||
private static boolean checkUUID(BluetoothDevice bluetoothDevice) {
|
||||
ParcelUuid[] AIRPODS_UUIDS = {
|
||||
ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"),
|
||||
ParcelUuid.fromString("2a72e02b-7b99-778f-014d-ad0b7221ec74")
|
||||
};
|
||||
ParcelUuid[] uuids = bluetoothDevice.getUuids();
|
||||
|
||||
if (uuids == null)
|
||||
if (uuids == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ParcelUuid u : uuids)
|
||||
for (ParcelUuid v : AIRPODS_UUIDS)
|
||||
if (u.equals(v)) return true;
|
||||
for (ParcelUuid u : uuids) {
|
||||
for (ParcelUuid v : AIRPODS_UUIDS) {
|
||||
if (u.equals(v)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy () {
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
unregisterBtReceiver();
|
||||
unregisterScreenReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand (Intent intent, int flags, int startId) {
|
||||
if (n == null || !n.isAlive()) {
|
||||
n = new NotificationThread(this) {
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (mNotificationThread == null || !mNotificationThread.isAlive()) {
|
||||
mNotificationThread = new NotificationThread(this) {
|
||||
@Override
|
||||
public boolean isConnected () {
|
||||
return maybeConnected;
|
||||
public boolean isConnected() {
|
||||
return mMaybeConnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PodsStatus getStatus () {
|
||||
return status;
|
||||
public PodsStatus getStatus() {
|
||||
return mStatus;
|
||||
}
|
||||
};
|
||||
n.start();
|
||||
mNotificationThread.start();
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
// Foreground service for background notification (confusing I know).
|
||||
// Only enabled for API30+
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private Notification createBackgroundNotification () {
|
||||
private Notification createBackgroundNotification() {
|
||||
final String notChannelID = "FOREGROUND_ID";
|
||||
|
||||
NotificationManager notManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
@@ -304,26 +309,25 @@ public class PodsService extends Service {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void unregisterBtReceiver () {
|
||||
private void unregisterBtReceiver() {
|
||||
try {
|
||||
if (btReceiver != null) {
|
||||
unregisterReceiver(btReceiver);
|
||||
btReceiver = null;
|
||||
if (mBluetoothReceiver != null) {
|
||||
unregisterReceiver(mBluetoothReceiver);
|
||||
mBluetoothReceiver = null;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Logger.error(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterScreenReceiver () {
|
||||
private void unregisterScreenReceiver() {
|
||||
try {
|
||||
if (screenReceiver != null) {
|
||||
unregisterReceiver(screenReceiver);
|
||||
screenReceiver = null;
|
||||
if (mScreenReceiver != null) {
|
||||
unregisterReceiver(mScreenReceiver);
|
||||
mScreenReceiver = null;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Logger.error(t);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package com.dosse.airpods.pods;
|
||||
|
||||
import com.dosse.airpods.pods.data.AirPods1;
|
||||
import com.dosse.airpods.pods.data.AirPods2;
|
||||
import com.dosse.airpods.pods.data.AirPods3;
|
||||
import com.dosse.airpods.pods.data.AirPodsMax;
|
||||
import com.dosse.airpods.pods.data.AirPodsPro;
|
||||
import com.dosse.airpods.pods.data.BeatsFlex;
|
||||
import com.dosse.airpods.pods.data.BeatsSolo3;
|
||||
import com.dosse.airpods.pods.data.BeatsStudio3;
|
||||
import com.dosse.airpods.pods.data.BeatsX;
|
||||
import com.dosse.airpods.pods.data.IPods;
|
||||
import com.dosse.airpods.pods.data.Pod;
|
||||
import com.dosse.airpods.pods.data.Powerbeats3;
|
||||
import com.dosse.airpods.pods.data.PowerbeatsPro;
|
||||
import com.dosse.airpods.pods.data.RegularPods;
|
||||
import com.dosse.airpods.pods.models.AirPods1;
|
||||
import com.dosse.airpods.pods.models.AirPods2;
|
||||
import com.dosse.airpods.pods.models.AirPods3;
|
||||
import com.dosse.airpods.pods.models.AirPodsMax;
|
||||
import com.dosse.airpods.pods.models.AirPodsPro;
|
||||
import com.dosse.airpods.pods.models.AirPodsPro2;
|
||||
import com.dosse.airpods.pods.models.AirPodsPro3;
|
||||
import com.dosse.airpods.pods.models.BeatsFlex;
|
||||
import com.dosse.airpods.pods.models.BeatsSolo3;
|
||||
import com.dosse.airpods.pods.models.BeatsStudio3;
|
||||
import com.dosse.airpods.pods.models.BeatsX;
|
||||
import com.dosse.airpods.pods.models.IPods;
|
||||
import com.dosse.airpods.pods.models.Powerbeats3;
|
||||
import com.dosse.airpods.pods.models.PowerbeatsPro;
|
||||
import com.dosse.airpods.pods.models.RegularPods;
|
||||
|
||||
/**
|
||||
* Decoding the beacon:
|
||||
@@ -27,21 +28,20 @@ import com.dosse.airpods.pods.data.RegularPods;
|
||||
* 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 model
|
||||
*
|
||||
* <p>
|
||||
* Notes:
|
||||
* 1) - isFlipped set by bit 1 of 10th character in the string; seems to be related to in-ear detection;
|
||||
*/
|
||||
public class PodsStatus {
|
||||
|
||||
public static final PodsStatus DISCONNECTED = new PodsStatus();
|
||||
|
||||
private IPods pods;
|
||||
private final long timestamp = System.currentTimeMillis();
|
||||
private IPods mPods;
|
||||
private final long mTimestamp = System.currentTimeMillis();
|
||||
|
||||
public PodsStatus () {
|
||||
private PodsStatus() {
|
||||
}
|
||||
|
||||
public PodsStatus (String status) {
|
||||
public PodsStatus(String status) {
|
||||
if (status == null)
|
||||
return;
|
||||
|
||||
@@ -74,49 +74,53 @@ public class PodsStatus {
|
||||
|
||||
// Detect which model
|
||||
if ("0220".equals(idFull)) {
|
||||
pods = new AirPods1(leftPod, rightPod, casePod); // Airpods 1st gen
|
||||
mPods = new AirPods1(leftPod, rightPod, casePod); // Airpods 1st gen
|
||||
} else if ("0F20".equals(idFull)) {
|
||||
pods = new AirPods2(leftPod, rightPod, casePod); // Airpods 2nd gen
|
||||
mPods = new AirPods2(leftPod, rightPod, casePod); // Airpods 2nd gen
|
||||
} else if ("1320".equals(idFull)) {
|
||||
pods = new AirPods3(leftPod, rightPod, casePod); // Airpods 3rd gen
|
||||
mPods = new AirPods3(leftPod, rightPod, casePod); // Airpods 3rd gen
|
||||
} else if ("0E20".equals(idFull)) {
|
||||
pods = new AirPodsPro(leftPod, rightPod, casePod); // Airpods Pro
|
||||
mPods = new AirPodsPro(leftPod, rightPod, casePod); // Airpods Pro
|
||||
} else if ("1420".equals(idFull) || "2420".equals(idFull)) {
|
||||
mPods = new AirPodsPro2(leftPod, rightPod, casePod); // Airpods Pro 2
|
||||
} else if ("2720".equals(idFull)) {
|
||||
mPods = new AirPodsPro3(leftPod, rightPod, casePod); // Airpods Pro 3
|
||||
} else if ('A' == idSingle) {
|
||||
pods = new AirPodsMax(singlePod); // Airpods Max
|
||||
mPods = new AirPodsMax(singlePod); // Airpods Max
|
||||
} else if ('B' == idSingle) {
|
||||
pods = new PowerbeatsPro(leftPod, rightPod, casePod); // Powerbeats Pro
|
||||
mPods = new PowerbeatsPro(leftPod, rightPod, casePod); // Powerbeats Pro
|
||||
} else if ("0520".equals(idFull)) {
|
||||
pods = new BeatsX(singlePod); // Beats X
|
||||
mPods = new BeatsX(singlePod); // Beats X
|
||||
} else if ("1020".equals(idFull)) {
|
||||
pods = new BeatsFlex(singlePod); // Beats Flex
|
||||
mPods = new BeatsFlex(singlePod); // Beats Flex
|
||||
} else if ("0620".equals(idFull)) {
|
||||
pods = new BeatsSolo3(singlePod); // Beats Solo 3
|
||||
mPods = new BeatsSolo3(singlePod); // Beats Solo 3
|
||||
} else if ('9' == idSingle) {
|
||||
pods = new BeatsStudio3(singlePod); // Beats Studio 3
|
||||
mPods = new BeatsStudio3(singlePod); // Beats Studio 3
|
||||
} else if ("0320".equals(idFull)) {
|
||||
pods = new Powerbeats3(singlePod); // Powerbeats 3
|
||||
mPods = new Powerbeats3(singlePod); // Powerbeats 3
|
||||
} else {
|
||||
pods = new RegularPods(leftPod, rightPod, casePod); // Unknown
|
||||
mPods = new RegularPods(leftPod, rightPod, casePod); // Unknown
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isFlipped (String str) {
|
||||
public static boolean isFlipped(String str) {
|
||||
return (Integer.parseInt("" + str.charAt(10), 16) & 0x02) == 0;
|
||||
}
|
||||
|
||||
public IPods getAirpods () {
|
||||
return pods;
|
||||
public IPods getAirpods() {
|
||||
return mPods;
|
||||
}
|
||||
|
||||
public boolean isAllDisconnected () {
|
||||
if (this == DISCONNECTED)
|
||||
public boolean isAllDisconnected() {
|
||||
if (this == DISCONNECTED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pods.isDisconnected();
|
||||
return mPods.isDisconnected();
|
||||
}
|
||||
|
||||
public long getTimestamp () {
|
||||
return timestamp;
|
||||
public long getTimestamp() {
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,18 +24,17 @@ import java.util.Objects;
|
||||
* - Decode...
|
||||
*/
|
||||
public abstract class PodsStatusScanCallback extends ScanCallback {
|
||||
private static final long RECENT_BEACONS_MAX_T_NS = 10000000000L; //10s
|
||||
|
||||
public static final long RECENT_BEACONS_MAX_T_NS = 10000000000L; //10s
|
||||
private static final int AIRPODS_MANUFACTURER = 76;
|
||||
private static final int AIRPODS_DATA_LENGTH = 27;
|
||||
private static final int MIN_RSSI = -60;
|
||||
|
||||
public static final int AIRPODS_MANUFACTURER = 76;
|
||||
public static final int AIRPODS_DATA_LENGTH = 27;
|
||||
public static final int MIN_RSSI = -60;
|
||||
private final List<ScanResult> mRecentBeacons = new ArrayList<>();
|
||||
|
||||
private final List<ScanResult> recentBeacons = new ArrayList<>();
|
||||
public abstract void onStatus(PodsStatus status);
|
||||
|
||||
public abstract void onStatus (PodsStatus status);
|
||||
|
||||
public static List<ScanFilter> getScanFilters () {
|
||||
public static List<ScanFilter> getScanFilters() {
|
||||
byte[] manufacturerData = new byte[AIRPODS_DATA_LENGTH];
|
||||
byte[] manufacturerDataMask = new byte[AIRPODS_DATA_LENGTH];
|
||||
|
||||
@@ -52,18 +51,20 @@ public abstract class PodsStatusScanCallback extends ScanCallback {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatchScanResults (List<ScanResult> scanResults) {
|
||||
for (ScanResult result : scanResults)
|
||||
public void onBatchScanResults(List<ScanResult> scanResults) {
|
||||
for (ScanResult result : scanResults) {
|
||||
onScanResult(-1, result);
|
||||
}
|
||||
|
||||
super.onBatchScanResults(scanResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanResult (int callbackType, ScanResult result) {
|
||||
public void onScanResult(int callbackType, ScanResult result) {
|
||||
try {
|
||||
if (!isAirpodsResult(result))
|
||||
if (!isAirpodsResult(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.getDevice().getAddress();
|
||||
|
||||
@@ -71,8 +72,9 @@ public abstract class PodsStatusScanCallback extends ScanCallback {
|
||||
Logger.debug(decodeResult(result));
|
||||
|
||||
result = getBestResult(result);
|
||||
if (result == null || result.getRssi() < MIN_RSSI)
|
||||
if (result == null || result.getRssi() < MIN_RSSI) {
|
||||
return;
|
||||
}
|
||||
|
||||
PodsStatus status = new PodsStatus(decodeResult(result));
|
||||
onStatus(status);
|
||||
@@ -81,50 +83,56 @@ public abstract class PodsStatusScanCallback extends ScanCallback {
|
||||
}
|
||||
}
|
||||
|
||||
private ScanResult getBestResult (ScanResult result) {
|
||||
recentBeacons.add(result);
|
||||
private ScanResult getBestResult(ScanResult result) {
|
||||
mRecentBeacons.add(result);
|
||||
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--);
|
||||
for (int i = 0; i < mRecentBeacons.size(); i++) {
|
||||
if (SystemClock.elapsedRealtimeNanos() - mRecentBeacons.get(i).getTimestampNanos() > RECENT_BEACONS_MAX_T_NS) {
|
||||
mRecentBeacons.remove(i--);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strongestBeacon == null || strongestBeacon.getRssi() < recentBeacons.get(i).getRssi())
|
||||
strongestBeacon = recentBeacons.get(i);
|
||||
if (strongestBeacon == null || strongestBeacon.getRssi() < mRecentBeacons.get(i).getRssi()) {
|
||||
strongestBeacon = mRecentBeacons.get(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (strongestBeacon != null && Objects.equals(strongestBeacon.getDevice().getAddress(), result.getDevice().getAddress()))
|
||||
if (strongestBeacon != null && Objects.equals(strongestBeacon.getDevice().getAddress(), result.getDevice().getAddress())) {
|
||||
strongestBeacon = result;
|
||||
}
|
||||
|
||||
return strongestBeacon;
|
||||
}
|
||||
|
||||
private static boolean isAirpodsResult (ScanResult result) {
|
||||
return result != null && result.getScanRecord() != null && isDataValid(result.getScanRecord().getManufacturerSpecificData(AIRPODS_MANUFACTURER));
|
||||
private static boolean isAirpodsResult(ScanResult result) {
|
||||
return result != null
|
||||
&& result.getScanRecord() != null
|
||||
&& isDataValid(result.getScanRecord().getManufacturerSpecificData(AIRPODS_MANUFACTURER));
|
||||
}
|
||||
|
||||
private static boolean isDataValid (byte[] data) {
|
||||
private static boolean isDataValid(byte[] data) {
|
||||
return data != null && data.length == AIRPODS_DATA_LENGTH;
|
||||
}
|
||||
|
||||
private static String decodeResult (ScanResult result) {
|
||||
private static String decodeResult(ScanResult result) {
|
||||
if (result != null && result.getScanRecord() != null) {
|
||||
byte[] data = result.getScanRecord().getManufacturerSpecificData(AIRPODS_MANUFACTURER);
|
||||
if (isDataValid(data))
|
||||
if (isDataValid(data)) {
|
||||
return decodeHex(data);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String decodeHex (byte[] bArr) {
|
||||
private static String decodeHex(byte[] byteArray) {
|
||||
StringBuilder ret = new StringBuilder();
|
||||
|
||||
for (byte b : bArr)
|
||||
for (byte b : byteArray) {
|
||||
ret.append(String.format("%02X", b));
|
||||
}
|
||||
|
||||
return ret.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
|
||||
public class AirPods1 extends RegularPods {
|
||||
|
||||
public AirPods1 (Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_AIRPODS_GEN1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
|
||||
public class AirPods2 extends RegularPods {
|
||||
|
||||
public AirPods2 (Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_AIRPODS_GEN2;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
|
||||
public interface IPods {
|
||||
|
||||
String MODEL_AIRPODS_GEN1 = "airpods1";
|
||||
String MODEL_AIRPODS_GEN2 = "airpods2";
|
||||
String MODEL_AIRPODS_GEN3 = "airpods3";
|
||||
String MODEL_AIRPODS_PRO = "airpodspro";
|
||||
String MODEL_AIRPODS_MAX = "airpodsmax";
|
||||
String MODEL_BEATS_X = "beatsx";
|
||||
String MODEL_BEATS_FLEX = "beatsflex";
|
||||
String MODEL_BEATS_SOLO_3 = "beatssolo3";
|
||||
String MODEL_BEATS_STUDIO_3 = "beatsstudio3";
|
||||
String MODEL_POWERBEATS_3 = "powerbeats3";
|
||||
String MODEL_POWERBEATS_PRO = "powerbeatspro";
|
||||
String MODEL_UNKNOWN = "unknown";
|
||||
|
||||
|
||||
String getModel ();
|
||||
|
||||
boolean isSingle ();
|
||||
|
||||
boolean isDisconnected ();
|
||||
|
||||
String parseStatusForLogger ();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class AirPods1 extends RegularPods {
|
||||
public AirPods1(Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel() {
|
||||
return Constants.MODEL_AIRPODS_GEN1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class AirPods2 extends RegularPods {
|
||||
public AirPods2(Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel() {
|
||||
return Constants.MODEL_AIRPODS_GEN2;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class AirPods3 extends RegularPods {
|
||||
|
||||
public AirPods3(Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_AIRPODS_GEN3;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_AIRPODS_GEN3;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class AirPodsMax extends SinglePods {
|
||||
|
||||
public AirPodsMax (Pod singlePod) {
|
||||
public AirPodsMax(Pod singlePod) {
|
||||
super(singlePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDrawable () {
|
||||
public int getDrawable() {
|
||||
return getPod().isConnected() ? R.drawable.podmax : R.drawable.podmax_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_AIRPODS_MAX;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_AIRPODS_MAX;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +1,30 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class AirPodsPro extends RegularPods {
|
||||
|
||||
public AirPodsPro (Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
public AirPodsPro(Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeftDrawable () {
|
||||
public int getLeftDrawable() {
|
||||
return getPod(LEFT).isConnected() ? R.drawable.podpro : R.drawable.podpro_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRightDrawable () {
|
||||
public int getRightDrawable() {
|
||||
return getPod(RIGHT).isConnected() ? R.drawable.podpro : R.drawable.podpro_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCaseDrawable () {
|
||||
public int getCaseDrawable() {
|
||||
return getPod(CASE).isConnected() ? R.drawable.podpro_case : R.drawable.podpro_case_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_AIRPODS_PRO;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_AIRPODS_PRO;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class AirPodsPro2 extends AirPodsPro {
|
||||
public AirPodsPro2(Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel() {
|
||||
return Constants.MODEL_AIRPODS_PRO_2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class AirPodsPro3 extends AirPodsPro {
|
||||
public AirPodsPro3(Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeftDrawable() {
|
||||
return getPod(LEFT).isConnected() ? R.drawable.podpro3 : R.drawable.podpro3_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRightDrawable() {
|
||||
return getPod(RIGHT).isConnected() ? R.drawable.podpro3 : R.drawable.podpro3_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel() {
|
||||
return Constants.MODEL_AIRPODS_PRO_3;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class BeatsFlex extends SinglePods {
|
||||
|
||||
public BeatsFlex (Pod singlePod) {
|
||||
public BeatsFlex(Pod singlePod) {
|
||||
super(singlePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDrawable () {
|
||||
public int getDrawable() {
|
||||
return getPod().isConnected() ? R.drawable.beatsflex : R.drawable.beatsflex_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_BEATS_FLEX;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_BEATS_FLEX;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class BeatsSolo3 extends SinglePods {
|
||||
|
||||
public BeatsSolo3 (Pod singlePod) {
|
||||
public BeatsSolo3(Pod singlePod) {
|
||||
super(singlePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDrawable () {
|
||||
public int getDrawable() {
|
||||
return getPod().isConnected() ? R.drawable.beatssolo3 : R.drawable.beatssolo3_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_BEATS_SOLO_3;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_BEATS_SOLO_3;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class BeatsStudio3 extends SinglePods {
|
||||
|
||||
public BeatsStudio3 (Pod singlePod) {
|
||||
public BeatsStudio3(Pod singlePod) {
|
||||
super(singlePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDrawable () {
|
||||
public int getDrawable() {
|
||||
return getPod().isConnected() ? R.drawable.beatsstudio3 : R.drawable.beatsstudio3_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_BEATS_STUDIO_3;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_BEATS_STUDIO_3;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class BeatsX extends SinglePods {
|
||||
|
||||
public BeatsX (Pod singlePod) {
|
||||
public BeatsX(Pod singlePod) {
|
||||
super(singlePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDrawable () {
|
||||
public int getDrawable() {
|
||||
return getPod().isConnected() ? R.drawable.beatsx : R.drawable.beatsx_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_BEATS_X;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_BEATS_X;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
public final class Constants {
|
||||
static final String MODEL_AIRPODS_GEN1 = "airpods1";
|
||||
static final String MODEL_AIRPODS_GEN2 = "airpods2";
|
||||
static final String MODEL_AIRPODS_GEN3 = "airpods3";
|
||||
static final String MODEL_AIRPODS_PRO = "airpodspro";
|
||||
static final String MODEL_AIRPODS_PRO_2 = "airpodspro2";
|
||||
static final String MODEL_AIRPODS_PRO_3 = "airpodspro3";
|
||||
static final String MODEL_AIRPODS_MAX = "airpodsmax";
|
||||
static final String MODEL_BEATS_X = "beatsx";
|
||||
static final String MODEL_BEATS_FLEX = "beatsflex";
|
||||
static final String MODEL_BEATS_SOLO_3 = "beatssolo3";
|
||||
static final String MODEL_BEATS_STUDIO_3 = "beatsstudio3";
|
||||
static final String MODEL_POWERBEATS_3 = "powerbeats3";
|
||||
static final String MODEL_POWERBEATS_PRO = "powerbeatspro";
|
||||
static final String MODEL_UNKNOWN = "unknown";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
public interface IPods {
|
||||
String getModel();
|
||||
|
||||
boolean isSingle();
|
||||
|
||||
boolean isDisconnected();
|
||||
|
||||
String parseStatusForLogger();
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class Powerbeats3 extends SinglePods {
|
||||
|
||||
public Powerbeats3 (Pod singlePod) {
|
||||
public Powerbeats3(Pod singlePod) {
|
||||
super(singlePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDrawable () {
|
||||
public int getDrawable() {
|
||||
return getPod().isConnected() ? R.drawable.powerbeats3 : R.drawable.powerbeats3_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_POWERBEATS_3;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_POWERBEATS_3;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +1,30 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
public class PowerbeatsPro extends RegularPods {
|
||||
|
||||
public PowerbeatsPro (Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
public PowerbeatsPro(Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
super(leftPod, rightPod, casePod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeftDrawable () {
|
||||
public int getLeftDrawable() {
|
||||
return getPod(LEFT).isConnected() ? R.drawable.powerbeatspro : R.drawable.powerbeatspro_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRightDrawable () {
|
||||
public int getRightDrawable() {
|
||||
return getPod(RIGHT).isConnected() ? R.drawable.powerbeatspro : R.drawable.powerbeatspro_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCaseDrawable () {
|
||||
public int getCaseDrawable() {
|
||||
return getPod(CASE).isConnected() ? R.drawable.powerbeatspro_case : R.drawable.powerbeatspro_case_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_POWERBEATS_PRO;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_POWERBEATS_PRO;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +1,73 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class RegularPods implements IPods {
|
||||
|
||||
public static final int LEFT = 0, RIGHT = 1, CASE = 2;
|
||||
|
||||
private final Pod[] pods;
|
||||
|
||||
public RegularPods (Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
public RegularPods(Pod leftPod, Pod rightPod, Pod casePod) {
|
||||
this.pods = new Pod[] {leftPod, rightPod, casePod};
|
||||
}
|
||||
|
||||
public Pod getPod (int pos) {
|
||||
public Pod getPod(int pos) {
|
||||
return pods[pos];
|
||||
}
|
||||
|
||||
public String getParsedStatus (int pos) {
|
||||
public String getParsedStatus(int pos) {
|
||||
return pods[pos].parseStatus();
|
||||
}
|
||||
|
||||
public int getInEarVisibility (int pos) {
|
||||
public int getInEarVisibility(int pos) {
|
||||
return pods[pos].inEarVisibility();
|
||||
}
|
||||
|
||||
public int getBatImgVisibility (int pos) {
|
||||
public int getBatImgVisibility(int pos) {
|
||||
return pods[pos].batImgVisibility();
|
||||
}
|
||||
|
||||
public int getBatImgSrcId (int pos) {
|
||||
public int getBatImgSrcId(int pos) {
|
||||
return pods[pos].batImgSrcId();
|
||||
}
|
||||
|
||||
public int getLeftDrawable () {
|
||||
public int getLeftDrawable() {
|
||||
return getPod(LEFT).isConnected() ? R.drawable.pod : R.drawable.pod_disconnected;
|
||||
}
|
||||
|
||||
public int getRightDrawable () {
|
||||
public int getRightDrawable() {
|
||||
return getPod(RIGHT).isConnected() ? R.drawable.pod : R.drawable.pod_disconnected;
|
||||
}
|
||||
|
||||
public int getCaseDrawable () {
|
||||
public int getCaseDrawable() {
|
||||
return getPod(CASE).isConnected() ? R.drawable.pod_case : R.drawable.pod_case_disconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel () {
|
||||
return MODEL_UNKNOWN;
|
||||
public String getModel() {
|
||||
return Constants.MODEL_UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingle () {
|
||||
public boolean isSingle() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisconnected () {
|
||||
public boolean isDisconnected() {
|
||||
return pods[LEFT].isDisconnected() &&
|
||||
pods[RIGHT].isDisconnected() &&
|
||||
pods[CASE].isDisconnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parseStatusForLogger () {
|
||||
public String parseStatusForLogger() {
|
||||
return String.format(Locale.getDefault(), "Left: %d%s%s Right: %d%s%s Case: %d%s Model: %s",
|
||||
pods[LEFT].getStatus(), pods[LEFT].isCharging() ? "+" : "", pods[LEFT].isInEar() ? "$" : "",
|
||||
pods[RIGHT].getStatus(), pods[RIGHT].isCharging() ? "+" : "", pods[RIGHT].isInEar() ? "$" : "",
|
||||
pods[CASE].getStatus(), pods[CASE].isCharging() ? "+" : "", getModel());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +1,46 @@
|
||||
package com.dosse.airpods.pods.data;
|
||||
package com.dosse.airpods.pods.models;
|
||||
|
||||
import com.dosse.airpods.pods.Pod;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class SinglePods implements IPods {
|
||||
|
||||
public abstract int getDrawable ();
|
||||
public abstract int getDrawable();
|
||||
|
||||
private final Pod pod;
|
||||
|
||||
public SinglePods (Pod pod) {
|
||||
public SinglePods(Pod pod) {
|
||||
this.pod = pod;
|
||||
}
|
||||
|
||||
public Pod getPod () {
|
||||
public Pod getPod() {
|
||||
return pod;
|
||||
}
|
||||
|
||||
public String getParsedStatus () {
|
||||
public String getParsedStatus() {
|
||||
return pod.parseStatus();
|
||||
}
|
||||
|
||||
public int getBatImgVisibility () {
|
||||
public int getBatImgVisibility() {
|
||||
return pod.batImgVisibility();
|
||||
}
|
||||
|
||||
public int getBatImgSrcId () {
|
||||
public int getBatImgSrcId() {
|
||||
return pod.batImgSrcId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingle () {
|
||||
public boolean isSingle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisconnected () {
|
||||
public boolean isDisconnected() {
|
||||
return pod.isDisconnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parseStatusForLogger () {
|
||||
public String parseStatusForLogger() {
|
||||
return String.format(Locale.getDefault(), "Battery: %d%s Model: %s", pod.getStatus(), pod.isCharging() ? "+" : "", getModel());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,23 +4,27 @@ import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
|
||||
public abstract class BluetoothListener implements BluetoothProfile.ServiceListener {
|
||||
public abstract boolean onConnect(BluetoothDevice bluetoothDevice);
|
||||
|
||||
public abstract boolean onConnect (BluetoothDevice bluetoothDevice);
|
||||
|
||||
public abstract void onDisconnect ();
|
||||
public abstract void onDisconnect();
|
||||
|
||||
@Override
|
||||
public void onServiceConnected (int profile, BluetoothProfile bluetoothProfile) {
|
||||
if (profile == BluetoothProfile.HEADSET)
|
||||
for (BluetoothDevice device : bluetoothProfile.getConnectedDevices())
|
||||
if (onConnect(device))
|
||||
break;
|
||||
public void onServiceConnected(int profile, BluetoothProfile bluetoothProfile) {
|
||||
if (profile != BluetoothProfile.HEADSET) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (BluetoothDevice device : bluetoothProfile.getConnectedDevices()) {
|
||||
if (onConnect(device)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected (int profile) {
|
||||
if (profile == BluetoothProfile.HEADSET)
|
||||
public void onServiceDisconnected(int profile) {
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
onDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,20 +8,19 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
public abstract class BluetoothReceiver extends BroadcastReceiver {
|
||||
public abstract void onStart();
|
||||
|
||||
public abstract void onStart ();
|
||||
public abstract void onStop();
|
||||
|
||||
public abstract void onStop ();
|
||||
public abstract void onConnect(BluetoothDevice bluetoothDevice);
|
||||
|
||||
public abstract void onConnect (BluetoothDevice bluetoothDevice);
|
||||
|
||||
public abstract void onDisconnect (BluetoothDevice bluetoothDevice);
|
||||
public abstract void onDisconnect(BluetoothDevice bluetoothDevice);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static IntentFilter buildFilter () {
|
||||
public static IntentFilter buildFilter() {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction("android.bluetooth.device.action.ACL_CONNECTED");
|
||||
intentFilter.addAction("android.bluetooth.device.action.ACL_DISCONNECTED");
|
||||
@@ -38,7 +37,7 @@ public abstract class BluetoothReceiver extends BroadcastReceiver {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive (Context context, Intent intent) {
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
BluetoothDevice bluetoothDevice = intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
|
||||
String action = intent.getAction();
|
||||
|
||||
@@ -46,24 +45,28 @@ public abstract class BluetoothReceiver extends BroadcastReceiver {
|
||||
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
|
||||
|
||||
// Bluetooth turned off, stop scanner and remove notification.
|
||||
if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF)
|
||||
if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) {
|
||||
onStop();
|
||||
}
|
||||
|
||||
// Bluetooth turned on, start/restart scanner.
|
||||
if (state == BluetoothAdapter.STATE_ON)
|
||||
if (state == BluetoothAdapter.STATE_ON) {
|
||||
onStart();
|
||||
}
|
||||
}
|
||||
|
||||
// Airpods filter
|
||||
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
||||
// Airpods connected, show notification.
|
||||
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action))
|
||||
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
|
||||
onConnect(bluetoothDevice);
|
||||
}
|
||||
|
||||
// Airpods disconnected, remove notification but leave the scanner going.
|
||||
if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action) || BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED.equals(action))
|
||||
if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)
|
||||
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED.equals(action)) {
|
||||
onDisconnect(bluetoothDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,27 +5,31 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class ScreenReceiver extends BroadcastReceiver {
|
||||
public abstract void onStart();
|
||||
|
||||
public abstract void onStart ();
|
||||
public abstract void onStop();
|
||||
|
||||
public abstract void onStop ();
|
||||
|
||||
public static IntentFilter buildFilter () {
|
||||
public static IntentFilter buildFilter() {
|
||||
IntentFilter screenIntentFilter = new IntentFilter();
|
||||
|
||||
screenIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
|
||||
screenIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
|
||||
return screenIntentFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive (Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case Intent.ACTION_SCREEN_OFF: onStop();
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (Objects.requireNonNull(intent.getAction())) {
|
||||
case Intent.ACTION_SCREEN_OFF:
|
||||
onStop();
|
||||
break;
|
||||
case Intent.ACTION_SCREEN_ON: onStart();
|
||||
case Intent.ACTION_SCREEN_ON:
|
||||
onStart();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.dosse.airpods.receivers;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import com.dosse.airpods.pods.PodsService;
|
||||
|
||||
@@ -13,9 +12,8 @@ import java.util.Objects;
|
||||
* A simple startup class that starts the service when the device is booted, or after an update
|
||||
*/
|
||||
public class StartupReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive (Context context, Intent intent) {
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (Objects.requireNonNull(intent.getAction())) {
|
||||
case Intent.ACTION_MY_PACKAGE_REPLACED:
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
@@ -24,14 +22,11 @@ public class StartupReceiver extends BroadcastReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
public static void startPodsService (Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
context.startForegroundService(new Intent(context, PodsService.class));
|
||||
else
|
||||
context.startService(new Intent(context, PodsService.class));
|
||||
public static void startPodsService(Context context) {
|
||||
context.startForegroundService(new Intent(context, PodsService.class));
|
||||
}
|
||||
|
||||
public static void restartPodsService (Context context) {
|
||||
public static void restartPodsService(Context context) {
|
||||
context.stopService(new Intent(context, PodsService.class));
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
@@ -39,5 +34,4 @@ public class StartupReceiver extends BroadcastReceiver {
|
||||
}
|
||||
startPodsService(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,9 +10,8 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.dosse.airpods.R;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate (Bundle savedInstanceState) {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
@@ -42,5 +41,4 @@ public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
// ------------ END OF LEGAL DANGER ZONE ------------ //
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,32 +23,39 @@ import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class IntroActivity extends AppCompatActivity {
|
||||
private static final int STEP_PERMISSION_BLUETOOTH = 1;
|
||||
private static final int STEP_PERMISSION_BATTERY_OPTIMIZATION = 2;
|
||||
private static final int STEP_PERMISSION_LOCATION = 3;
|
||||
private static final int STEP_PERMISSION_BACKGROUND_LOCATION = 4;
|
||||
private static final int STEP_PERMISSION_NOTIFICATION = 5;
|
||||
|
||||
private Timer timer;
|
||||
private TextView msg;
|
||||
private Button btn;
|
||||
private static final int BLUETOOTH_REQUEST_CODE = 101;
|
||||
private static final int LOCATION_REQUEST_CODE = 102;
|
||||
private static final int BACKGROUND_LOCATION_REQUEST_CODE = 103;
|
||||
private static final int NOTIFICATION_REQUEST_CODE = 104;
|
||||
|
||||
private static final int STEP_PERMISSION_BLUETOOTH = 1, STEP_PERMISSION_BATTERY_OPTIMIZATION = 2, STEP_PERMISSION_LOCATION = 3, STEP_PERMISSION_BACKGROUND_LOCATION = 4;
|
||||
private static final int BLUETOOTH_REQUEST_CODE = 101, LOCATION_REQUEST_CODE = 102, BACKGROUND_LOCATION_REQUEST_CODE = 103;
|
||||
private Timer mTimer;
|
||||
private TextView mPermissionsMessage;
|
||||
private Button mPermissionsButton;
|
||||
|
||||
@Override
|
||||
protected void onCreate (Bundle savedInstanceState) {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_intro);
|
||||
|
||||
msg = findViewById(R.id.permsMsg);
|
||||
btn = findViewById(R.id.permsBtn);
|
||||
mPermissionsMessage = findViewById(R.id.permsMsg);
|
||||
mPermissionsButton = findViewById(R.id.permsBtn);
|
||||
|
||||
// Wait for permissions to be granted.
|
||||
// When they are granted, go to MainActivity.
|
||||
timer = new Timer();
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
mTimer = new Timer();
|
||||
mTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run () {
|
||||
public void run() {
|
||||
initScreen();
|
||||
|
||||
if (PermissionUtils.checkAllPermissions(IntroActivity.this)) {
|
||||
timer.cancel();
|
||||
mTimer.cancel();
|
||||
startActivity(new Intent(IntroActivity.this, MainActivity.class));
|
||||
finish();
|
||||
}
|
||||
@@ -59,42 +66,49 @@ public class IntroActivity extends AppCompatActivity {
|
||||
|
||||
// Activity destroyed (or screen rotated). destroy the timer too
|
||||
@Override
|
||||
protected void onDestroy () {
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (timer != null)
|
||||
timer.cancel();
|
||||
if (mTimer != null)
|
||||
mTimer.cancel();
|
||||
}
|
||||
|
||||
private int getPermissionState () {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) if (!PermissionUtils.getBluetoothPermissions(this))
|
||||
private int getPermissionState() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !PermissionUtils.getBluetoothPermissions(this)) {
|
||||
return STEP_PERMISSION_BLUETOOTH;
|
||||
if (!PermissionUtils.getBatteryOptimizationsPermission(this))
|
||||
}
|
||||
if (!PermissionUtils.getBatteryOptimizationsPermission(this)) {
|
||||
return STEP_PERMISSION_BATTERY_OPTIMIZATION;
|
||||
if (!PermissionUtils.getFineLocationPermission(this))
|
||||
}
|
||||
if (!PermissionUtils.getFineLocationPermission(this)) {
|
||||
return STEP_PERMISSION_LOCATION;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) if (!PermissionUtils.getBackgroundLocationPermission(this))
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !PermissionUtils.getBackgroundLocationPermission(this)) {
|
||||
return STEP_PERMISSION_BACKGROUND_LOCATION;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !PermissionUtils.getNotificationPermissions(this)) {
|
||||
return STEP_PERMISSION_NOTIFICATION;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
private void initScreen () {
|
||||
private void initScreen() {
|
||||
int currentStep = getPermissionState() - ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) ? 0 : 1);
|
||||
int numOfSteps = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ? ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) ? 4 : 3) : 2;
|
||||
|
||||
runOnUiThread(() -> {
|
||||
switch (getPermissionState()) {
|
||||
case STEP_PERMISSION_BLUETOOTH:
|
||||
msg.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, numOfSteps, getString(R.string.intro_bt_perm)));
|
||||
btn.setOnClickListener(view -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
mPermissionsMessage.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, getNumberOfSteps(), getString(R.string.intro_bt_perm)));
|
||||
mPermissionsButton.setOnClickListener(view -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
requestPermissions(new String[] {Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}, BLUETOOTH_REQUEST_CODE);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case STEP_PERMISSION_BATTERY_OPTIMIZATION:
|
||||
msg.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, numOfSteps, getString(R.string.intro_bat_perm)));
|
||||
btn.setOnClickListener(view -> {
|
||||
mPermissionsMessage.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, getNumberOfSteps(), getString(R.string.intro_bat_perm)));
|
||||
mPermissionsButton.setOnClickListener(view -> {
|
||||
Intent intent = new Intent();
|
||||
getSystemService(Context.POWER_SERVICE);
|
||||
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
@@ -103,43 +117,78 @@ public class IntroActivity extends AppCompatActivity {
|
||||
});
|
||||
break;
|
||||
case STEP_PERMISSION_LOCATION:
|
||||
msg.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, numOfSteps, getString(R.string.intro_loc1_perm)));
|
||||
btn.setOnClickListener(view -> requestPermissions(new String[] {Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_REQUEST_CODE)); // Location (for BLE)
|
||||
mPermissionsMessage.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, getNumberOfSteps(), getString(R.string.intro_loc1_perm)));
|
||||
mPermissionsButton.setOnClickListener(view -> requestPermissions(new String[] {Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_REQUEST_CODE)); // Location (for BLE)
|
||||
break;
|
||||
case STEP_PERMISSION_BACKGROUND_LOCATION:
|
||||
msg.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, numOfSteps, getString(R.string.intro_loc2_perm)));
|
||||
btn.setOnClickListener(view -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) requestPermissions(new String[] {Manifest.permission.ACCESS_BACKGROUND_LOCATION}, BACKGROUND_LOCATION_REQUEST_CODE);
|
||||
mPermissionsMessage.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, getNumberOfSteps(), getString(R.string.intro_loc2_perm)));
|
||||
mPermissionsButton.setOnClickListener(view -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
requestPermissions(new String[] {Manifest.permission.ACCESS_BACKGROUND_LOCATION}, BACKGROUND_LOCATION_REQUEST_CODE);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case STEP_PERMISSION_NOTIFICATION:
|
||||
mPermissionsMessage.setText(String.format(Locale.getDefault(), "%s %d/%d: %s", getString(R.string.intro_step), currentStep, getNumberOfSteps(), getString(R.string.intro_notif_perm)));
|
||||
mPermissionsButton.setOnClickListener(view -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
requestPermissions(new String[] {Manifest.permission.POST_NOTIFICATIONS}, NOTIFICATION_REQUEST_CODE);
|
||||
}
|
||||
});
|
||||
}
|
||||
btn.setText(String.format(Locale.getDefault(), "%s (%d/%d)", getString(R.string.intro_allow), currentStep, numOfSteps));
|
||||
mPermissionsButton.setText(String.format(Locale.getDefault(), "%s (%d/%d)", getString(R.string.intro_allow), currentStep, getNumberOfSteps()));
|
||||
});
|
||||
}
|
||||
|
||||
private int getNumberOfSteps() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return 5;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return 4;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult (int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
switch (requestCode) {
|
||||
case BLUETOOTH_REQUEST_CODE:
|
||||
if (!hasAllPermissionsGranted(grantResults)) Toast.makeText(this, getString(R.string.msg_bt_perm), Toast.LENGTH_LONG).show();
|
||||
if (!hasAllPermissionsGranted(grantResults)) {
|
||||
Toast.makeText(this, getString(R.string.msg_bt_perm), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
break;
|
||||
case LOCATION_REQUEST_CODE:
|
||||
if (!hasAllPermissionsGranted(grantResults)) Toast.makeText(this, getString(R.string.msg_loc1_perm), Toast.LENGTH_LONG).show();
|
||||
if (!hasAllPermissionsGranted(grantResults)) {
|
||||
Toast.makeText(this, getString(R.string.msg_loc1_perm), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
break;
|
||||
case BACKGROUND_LOCATION_REQUEST_CODE:
|
||||
if (!hasAllPermissionsGranted(grantResults)) Toast.makeText(this, getString(R.string.msg_loc2_perm), Toast.LENGTH_LONG).show();
|
||||
if (!hasAllPermissionsGranted(grantResults)) {
|
||||
Toast.makeText(this, getString(R.string.msg_loc2_perm), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_REQUEST_CODE:
|
||||
if (!hasAllPermissionsGranted(grantResults)) {
|
||||
Toast.makeText(this, getString(R.string.msg_notif_perm), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean hasAllPermissionsGranted (@NonNull int[] grantResults) {
|
||||
for (int grantResult : grantResults)
|
||||
if (grantResult == PackageManager.PERMISSION_DENIED)
|
||||
public boolean hasAllPermissionsGranted(@NonNull int[] grantResults) {
|
||||
for (int grantResult : grantResults) {
|
||||
if (grantResult == PackageManager.PERMISSION_DENIED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,9 +19,8 @@ import com.dosse.airpods.utils.PermissionUtils;
|
||||
import java.util.Objects;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate (Bundle savedInstanceState) {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
@@ -30,6 +29,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (btAdapter == null || (btAdapter.isEnabled() && btAdapter.getBluetoothLeScanner() == null) || (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE))) {
|
||||
startActivity(new Intent(MainActivity.this, NoBTActivity.class));
|
||||
finish();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,18 +44,18 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu (Menu menu) {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected (@NonNull MenuItem item) {
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_ab_settings) {
|
||||
startActivity(new Intent(this, SettingsActivity.class)); // Settings icon clicked
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.dosse.airpods.R;
|
||||
|
||||
public class NoBTActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate (Bundle savedInstanceState) {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_no_bt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ import com.dosse.airpods.R;
|
||||
import com.dosse.airpods.receivers.StartupReceiver;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate (Bundle savedInstanceState) {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
getSupportFragmentManager()
|
||||
@@ -23,9 +22,8 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences (Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(R.xml.preference_screen, rootKey);
|
||||
|
||||
getPreference("batterySaver").setOnPreferenceClickListener(preference -> {
|
||||
@@ -46,10 +44,8 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private Preference getPreference (String key) {
|
||||
private Preference getPreference(String key) {
|
||||
return getPreferenceManager().findPreference(key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@ import android.util.Log;
|
||||
import com.dosse.airpods.BuildConfig;
|
||||
|
||||
public class Logger {
|
||||
|
||||
// Log is only displayed if this is a debug build, not release
|
||||
private static final boolean ENABLE_LOGGING = BuildConfig.DEBUG;
|
||||
public static final String TAG = "AirPods";
|
||||
|
||||
public static void debug (String msg) {
|
||||
if (ENABLE_LOGGING) Log.d(TAG, msg);
|
||||
public static void debug(String msg) {
|
||||
if (ENABLE_LOGGING) {
|
||||
Log.d(TAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static void error (Throwable t) {
|
||||
if (ENABLE_LOGGING) Log.e(TAG, "ERROR", t);
|
||||
public static void error(Throwable t) {
|
||||
if (ENABLE_LOGGING) {
|
||||
Log.e(TAG, "ERROR", t);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,31 +6,47 @@ import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.dosse.airpods.R;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class MIUIWarning {
|
||||
public static void show(Context context) {
|
||||
Context appContext = context.getApplicationContext();
|
||||
|
||||
public static void show (Context context) {
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> c = Class.forName("android.os.SystemProperties");
|
||||
String miuiVersion = (String)c.getMethod("get", String.class).invoke(c, "ro.miui.ui.version.code");
|
||||
if (miuiVersion != null && !miuiVersion.isEmpty()) {
|
||||
try {
|
||||
context.getApplicationContext().openFileInput("miuiwarn").close();
|
||||
} catch (Throwable ignored) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.miui_warning);
|
||||
builder.setMessage(R.string.miui_warning_desc);
|
||||
builder.setNeutralButton(R.string.miui_warning_continue, (dialog, which) -> dialog.dismiss());
|
||||
builder.setOnDismissListener(dialog -> {
|
||||
try {
|
||||
context.getApplicationContext().openFileOutput("miuiwarn", Context.MODE_PRIVATE).close();
|
||||
} catch (Throwable ignored2) {
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
String miuiVersion = getMiuiVersion();
|
||||
if (miuiVersion == null || miuiVersion.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
appContext.openFileInput("miuiwarn").close();
|
||||
} catch (Throwable ignored) {
|
||||
showAlertDialog(appContext);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private static String getMiuiVersion() throws ReflectiveOperationException {
|
||||
Class<?> systemPropertiesClazz = Class.forName("android.os.SystemProperties");
|
||||
Method systemPropertiesGet = systemPropertiesClazz.getMethod("get", String.class);
|
||||
systemPropertiesGet.setAccessible(true);
|
||||
|
||||
return (String)systemPropertiesGet.invoke(null, "ro.miui.ui.version.code");
|
||||
}
|
||||
|
||||
private static void showAlertDialog(Context context) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.miui_warning);
|
||||
builder.setMessage(R.string.miui_warning_desc);
|
||||
builder.setNeutralButton(R.string.miui_warning_continue, (dialog, which) -> dialog.dismiss());
|
||||
builder.setOnDismissListener(dialog -> {
|
||||
try {
|
||||
context.openFileOutput("miuiwarn", Context.MODE_PRIVATE).close();
|
||||
} catch (Throwable ignored2) {
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,47 +7,50 @@ import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static androidx.core.content.ContextCompat.checkSelfPermission;
|
||||
|
||||
public class PermissionUtils {
|
||||
public static boolean getBatteryOptimizationsPermission(Context context) {
|
||||
return context.getSystemService(PowerManager.class).isIgnoringBatteryOptimizations(context.getPackageName());
|
||||
}
|
||||
|
||||
public static boolean getBatteryOptimizationsPermission (Context context) {
|
||||
try {
|
||||
return Objects.requireNonNull(context.getSystemService(PowerManager.class)).isIgnoringBatteryOptimizations(context.getPackageName());
|
||||
} catch (Throwable ignored) {
|
||||
return true;
|
||||
}
|
||||
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
||||
public static boolean getNotificationPermissions(Context context) {
|
||||
return checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.S)
|
||||
public static boolean getBluetoothPermissions (Context context) {
|
||||
public static boolean getBluetoothPermissions(Context context) {
|
||||
return checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED
|
||||
&& checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public static boolean getFineLocationPermission (Context context) {
|
||||
public static boolean getFineLocationPermission(Context context) {
|
||||
return checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
public static boolean getBackgroundLocationPermission (Context context) {
|
||||
public static boolean getBackgroundLocationPermission(Context context) {
|
||||
return checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public static boolean getLocationPermissions (Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
public static boolean getLocationPermissions(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
return getFineLocationPermission(context) && getBackgroundLocationPermission(context);
|
||||
else
|
||||
return getFineLocationPermission(context);
|
||||
}
|
||||
|
||||
return getFineLocationPermission(context);
|
||||
}
|
||||
|
||||
public static boolean checkAllPermissions (Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
return getBatteryOptimizationsPermission(context) && getLocationPermissions(context) && getBluetoothPermissions(context);
|
||||
else
|
||||
public static boolean checkAllPermissions(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return getNotificationPermissions(context) && getBatteryOptimizationsPermission(context) && getLocationPermissions(context) && getBluetoothPermissions(context);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return getBatteryOptimizationsPermission(context) && getLocationPermissions(context);
|
||||
}
|
||||
}
|
||||
|
||||
return getBatteryOptimizationsPermission(context) && getLocationPermissions(context) && getBluetoothPermissions(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ import android.content.SharedPreferences;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
public class SharedPreferencesUtils {
|
||||
|
||||
public static boolean isSavingBattery (Context context) {
|
||||
public static boolean isSavingBattery(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return prefs.getBoolean("batterySaver", false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 551 KiB After Width: | Height: | Size: 484 KiB |
|
Before Width: | Height: | Size: 543 KiB After Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 130 KiB |
@@ -62,6 +62,16 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/airpodspro" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/airpodspro2" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/airpodspro3" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -55,6 +55,16 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/airpodspro" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/airpodspro2" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/airpodspro3" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 658 B After Width: | Height: | Size: 541 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 341 B After Width: | Height: | Size: 296 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 531 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1007 B |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="bg_noti_channel" translatable="false">Background Notification</string>
|
||||
<string name="bg_noti_title" tools:ignore="MissingTranslation">Běží na pozadí</string>
|
||||
<string name="bg_noti_text" tools:ignore="MissingTranslation">Stisknutím otevřete nastavení oznámení</string>
|
||||
|
||||
<string name="intro_title">Tato aplikace vám umožňuje zkontrolovat stav vašich AirPods na jakémkoli zařízení s Androidem</string>
|
||||
<string name="intro_message">Pro provoz je potřeba povolení k použití Bluetooth LE (místo) a povolení k běhu na pozadí</string>
|
||||
<string name="intro_allow">Povolit</string>
|
||||
<string name="intro_step" tools:ignore="MissingTranslation">Krok</string>
|
||||
<string name="intro_bt_perm" tools:ignore="MissingTranslation">Povolení Bluetooth</string>
|
||||
<string name="intro_bat_perm" tools:ignore="MissingTranslation">Optimalizace baterie</string>
|
||||
<string name="intro_loc1_perm" tools:ignore="MissingTranslation">Povolení polohy</string>
|
||||
<string name="intro_loc2_perm" tools:ignore="MissingTranslation">Povolení polohy v pozadí</string>
|
||||
<string name="intro_notif_perm" tools:ignore="MissingTranslation">Povolení notifikace</string>
|
||||
<string name="msg_bt_perm" tools:ignore="MissingTranslation">Nebylo uděleno povolení pro Bluetooth, povolte jej prosím v nastavení.</string>
|
||||
<string name="msg_loc1_perm" tools:ignore="MissingTranslation">Nebylo uděleno povolení polohy, povolte jej prosím v nastavení.</string>
|
||||
<string name="msg_loc2_perm" tools:ignore="MissingTranslation">Nebylo uděleno povolení polohy na pozadí, povolte jej prosím v nastavení.</string>
|
||||
<string name="msg_notif_perm" tools:ignore="MissingTranslation">Nebylo uděleno povolení notifikace, povolte jej prosím v nastavení.</string>
|
||||
|
||||
<string name="main_description">The app will show a notification when your AirPods are connected</string>
|
||||
<string name="supportedDevicesTitle"><u>Supported devices:</u></string>
|
||||
<string name="airpods1">• Apple AirPods 1st gen</string>
|
||||
<string name="airpods2">• Apple AirPods 2nd gen</string>
|
||||
<string name="airpods3" tools:ignore="MissingTranslation">• Apple AirPods 3rd gen</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro2" tools:ignore="MissingTranslation">• Apple AirPods Pro 2</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
<string name="beatssolo3">• Beats Solo 3</string>
|
||||
<string name="beatsstudio3">• Beats Studio 3</string>
|
||||
<string name="powerbeats3">• Powerbeats 3</string>
|
||||
<string name="powerbeatspro">• Powerbeats Pro</string>
|
||||
|
||||
<string name="miui_warning" tools:ignore="MissingTranslation">Detekováno MIUI</string>
|
||||
<string name="miui_warning_desc" tools:ignore="MissingTranslation">Na tomto zařízení běží ROM s známými problémy kompatibility. Můžete pokračovat, ale OpenPods nemusí fungovat správně.</string>
|
||||
<string name="miui_warning_continue" tools:ignore="MissingTranslation">Pokračovat</string>
|
||||
|
||||
<string name="noBtText">Toto zařízení nemá Bluetooth LE</string>
|
||||
|
||||
<string name="settings" tools:ignore="MissingTranslation">Nastavení</string>
|
||||
<string name="batterySaver">Spořič baterie (nedoporučeno)</string>
|
||||
<string name="batterySaver_desc">Povolte pokud Bluetooth spotřebovává velké množství baterie</string>
|
||||
<string name="restart" tools:ignore="MissingTranslation">Službu restartujte</string>
|
||||
<string name="restart_desc" tools:ignore="MissingTranslation">Udělejte to v případě, že se oznámení nezobrazuje</string>
|
||||
|
||||
<string name="about">O developerovi</string>
|
||||
<string name="about_dev">Vytvořili společně Federico Dossena a Itai Levin</string>
|
||||
<string name="website">webová stránka</string>
|
||||
<string name="donate">Darujte</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,14 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="bg_noti_channel" translatable="false">Background Notification</string>
|
||||
<string name="bg_noti_title" tools:ignore="MissingTranslation">Läuft im Hintergrund</string>
|
||||
<string name="bg_noti_text" tools:ignore="MissingTranslation">Anklicken, um die Benachrichtigungseinstellungen zu öffnen</string>
|
||||
|
||||
<string name="intro_title">Diese App erlaubt es dir, den Status deiner AirPods auf einem beliebigen Android-Gerät zu überprüfen</string>
|
||||
<string name="intro_message">Für den Dienst benötigen wir die Berechtigung zur Verwendung von Bluetooth LE (Standort) und die Berechtigung zum Betrieb im Hintergrund</string>
|
||||
<string name="intro_allow">Erlauben</string>
|
||||
<string name="intro_step" tools:ignore="MissingTranslation">Weiter</string>
|
||||
<string name="intro_bt_perm" tools:ignore="MissingTranslation">Bluetooth-Berechtigung</string>
|
||||
<string name="intro_bat_perm" tools:ignore="MissingTranslation">Akku-Optimierung</string>
|
||||
<string name="intro_loc1_perm" tools:ignore="MissingTranslation">Standortberechtigung</string>
|
||||
<string name="intro_loc2_perm" tools:ignore="MissingTranslation">Berechtigung für Standort im Hintergrund</string>
|
||||
<string name="intro_notif_perm" tools:ignore="MissingTranslation">Berechtigung für Benachrichtigung</string>
|
||||
<string name="msg_bt_perm" tools:ignore="MissingTranslation">Bluetooth-Berechtigung nicht erteilt, bitte in den Einstellungen aktivieren.</string>
|
||||
<string name="msg_loc1_perm" tools:ignore="MissingTranslation">Standortberechtigung nicht erteilt, bitte in den Einstellungen aktivieren.</string>
|
||||
<string name="msg_loc2_perm" tools:ignore="MissingTranslation">Berechtigung für Standort im Hintergrund nicht erteilt, bitte in den Einstellungen aktivieren.</string>
|
||||
<string name="msg_notif_perm" tools:ignore="MissingTranslation">Berechtigung für Benachrichtigung nicht erteilt, bitte in den Einstellungen aktivieren.</string>
|
||||
|
||||
<string name="main_description">Die App zeigt eine Benachrichtigung, wenn deine AirPods verbunden sind</string>
|
||||
<string name="supportedDevicesTitle"><u>Unterstützte Geräte:</u></string>
|
||||
<string name="airpods1">• Apple AirPods 1st gen</string>
|
||||
<string name="airpods2">• Apple AirPods 2nd gen</string>
|
||||
<string name="airpods1">• Apple AirPods 1</string>
|
||||
<string name="airpods2">• Apple AirPods 2</string>
|
||||
<string name="airpods3" tools:ignore="MissingTranslation">• Apple AirPods 3</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro2" tools:ignore="MissingTranslation">• Apple AirPods Pro 2</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
@@ -18,16 +36,20 @@
|
||||
<string name="powerbeatspro">• Powerbeats Pro</string>
|
||||
|
||||
<string name="miui_warning">MIUI erkannt</string>
|
||||
<string name="miui_warning_desc" tools:ignore="MissingTranslation">Die ROM, die auf diesem Gerät läuft, hat bekannte Kompatibilitätsprobleme. Du kannst fortfahren, aber OpenPods funktioniert möglicherweise nicht richtig.</string>
|
||||
<string name="miui_warning_continue">Fortsetzen</string>
|
||||
|
||||
<string name="noBtText">Es sieht so aus, als hätte dieses Gerät kein Bluetooth LE</string>
|
||||
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="batterySaver">Stromsparmodus (nicht empfohlen)</string>
|
||||
<string name="batterySaver_desc">Aktiviere diesen Modus, wenn Bluetooth zu viel Batterieleistung erfordert</string>
|
||||
<string name="batterySaver_desc">Aktiviere diesen Modus, wenn Bluetooth zu viel Akkuleistung erfordert</string>
|
||||
<string name="restart" tools:ignore="MissingTranslation">Dienst neu starten</string>
|
||||
<string name="restart_desc" tools:ignore="MissingTranslation">Wähle dies, wenn die Benachrichtigung nicht angezeigt wird</string>
|
||||
|
||||
<string name="about">Über</string>
|
||||
<string name="about_dev">Entwickelt von Federico Dossena und Itai Levin</string>
|
||||
<string name="website">Webseite</string>
|
||||
<string name="donate">Spenden</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="airpods1">• Apple AirPods 1ª generación</string>
|
||||
<string name="airpods2">• Apple AirPods 2ª generación</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
@@ -30,4 +31,5 @@
|
||||
<string name="about_dev">Desarrollado por Federico Dossena y Itai Levin</string>
|
||||
<string name="website">Página web</string>
|
||||
<string name="donate">Donar</string>
|
||||
<string name="airpodspro2">• Apple AirPods Pro 2</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<string name="airpods1">• Apple AirPods 1re génération</string>
|
||||
<string name="airpods2">• Apple AirPods 2e génération</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro2">• Apple AirPods Pro 2</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<string name="airpods1">• Apple AirPods 1st gen</string>
|
||||
<string name="airpods2">• Apple AirPods 2nd gen</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro2">• Apple AirPods Pro 2</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
<string name="supportedDevicesTitle"><u>מכשירים נתמכים</u></string>
|
||||
<string name="airpods1">• אפל איירפודס דור ראשון</string>
|
||||
<string name="airpods2">• אפל איירפודס דור שני</string>
|
||||
<string name="airpods3">• אפל איירפודס דור שלישי</string>
|
||||
<string name="airpodspro">• אפל איירפודס פרו</string>
|
||||
<string name="airpodspro2">• אפל איירפודס פרו 2</string>
|
||||
<string name="airpodspro3">• אפל איירפודס פרו 3</string>
|
||||
<string name="airpodsmax">• אפל איירפודס מקס</string>
|
||||
<string name="beatsx">• ביטס אקס</string>
|
||||
<string name="beatsflex">• ביטס פלקס</string>
|
||||
@@ -17,8 +20,8 @@
|
||||
<string name="powerbeats3">•פווארביטס 3</string>
|
||||
<string name="powerbeatspro">• פווארביטס פרו</string>
|
||||
|
||||
<string name="miui_warning">MIUI נמצא</string>
|
||||
<string name="miui_warning_desc">למערכת ההפעלה על מכשיר זה יש בעיות תאימות ידועות עם האפליקציה. אתה יכול להמשיך להשתמש ב-OpenPods אך לא בטוח שדברים יעבדו כמו שצריך.</string>
|
||||
<string name="miui_warning">MIUI זוהה</string>
|
||||
<string name="miui_warning_desc">למערכת ההפעלה במכשיר זה יש בעיות תאימות ידועות עם האפליקציה. אתה יכול להמשיך להשתמש ב-OpenPods אך לא בטוח שדברים יעבדו כמו שצריך.</string>
|
||||
<string name="miui_warning_continue">המשך</string>
|
||||
|
||||
<string name="noBtText">נראה כי מכשיר זה אינו תומך ב-Bluetooth LE</string>
|
||||
@@ -30,6 +33,5 @@
|
||||
<string name="about">אודות</string>
|
||||
<string name="about_dev">פותח על ידי פדריקו דוסנה ואיתי לוין</string>
|
||||
<string name="website">אתר המפתח</string>
|
||||
<string name="github">קוד מקור</string>
|
||||
<string name="donate">תרומה</string>
|
||||
</resources>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
<string name="airpods1">• Apple AirPods 1e gen.</string>
|
||||
<string name="airpods2">• Apple AirPods 2e gen.</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro2">• Apple AirPods Pro 2</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
@@ -43,6 +45,5 @@
|
||||
<string name="about">Over</string>
|
||||
<string name="about_dev">Gemaakt door Federico Dossena en Itai Levin</string>
|
||||
<string name="website">Website</string>
|
||||
<string name="github">GitHub</string>
|
||||
<string name="donate">Doneren</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="bg_noti_channel" translatable="false">Фоновое уведомление</string>
|
||||
<string name="bg_noti_title" tools:ignore="MissingTranslation">Работает в фоновом режиме</string>
|
||||
<string name="bg_noti_text" tools:ignore="MissingTranslation">Нажмите, чтобы войти в настройки уведомлений</string>
|
||||
|
||||
<string name="intro_title">Это приложение позволяет Вам проверить статус ваших AirPods на любом Android-устройстве</string>
|
||||
<string name="intro_message">Для работы нам нужно Bluetooth LE(местоположение) и разрешение на запуск в фоновом режиме</string>
|
||||
<string name="intro_allow">Разрешить</string>
|
||||
<string name="intro_step" tools:ignore="MissingTranslation">Шаг</string>
|
||||
<string name="intro_bt_perm" tools:ignore="MissingTranslation">Bluetooth</string>
|
||||
<string name="intro_bat_perm" tools:ignore="MissingTranslation">Оптимизация расхода батареи</string>
|
||||
<string name="intro_loc1_perm" tools:ignore="MissingTranslation">Местоположение</string>
|
||||
<string name="intro_loc2_perm" tools:ignore="MissingTranslation">Местоположение в фоновом режиме</string>
|
||||
<string name="intro_notif_perm" tools:ignore="MissingTranslation">Уведомления</string>
|
||||
<string name="msg_bt_perm" tools:ignore="MissingTranslation">Разрешение на использование Bluetooth не получено. Пожалуйста, выставьте его вручную в настройках приложения.</string>
|
||||
<string name="msg_loc1_perm" tools:ignore="MissingTranslation">Разрешение на использование местоположения не получено. Пожалуйста, выставьте его вручную в настройках приложения.</string>
|
||||
<string name="msg_loc2_perm" tools:ignore="MissingTranslation">Разрешение на использование местоположения в фоновом режиме не получено. Пожалуйста, выставьте его вручную в настройках приложения.</string>
|
||||
<string name="msg_notif_perm" tools:ignore="MissingTranslation">Разрешение на отображение уведомлений не получено. Пожалуйста, выставьте его вручную в настройках приложения.</string>
|
||||
|
||||
<string name="main_description">Приложение покажет уведомление, когда будет установлено соединение с AirPods</string>
|
||||
<string name="supportedDevicesTitle"><u>Поддерживаемые устройства:</u></string>
|
||||
<string name="airpods1">• Apple AirPods 1st gen</string>
|
||||
<string name="airpods2">• Apple AirPods 2nd gen</string>
|
||||
<string name="airpods3" tools:ignore="MissingTranslation">• Apple AirPods 3rd gen</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro2" tools:ignore="MissingTranslation">• Apple AirPods Pro 2</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
@@ -18,13 +36,16 @@
|
||||
<string name="powerbeatspro">• Powerbeats Pro</string>
|
||||
|
||||
<string name="miui_warning">MIUI обнаружено</string>
|
||||
<string name="miui_warning_continue">Продолжать</string>
|
||||
<string name="miui_warning_desc">Прошивка, запущенная на вашем устройстве имеет известные неполадки с совместимостью с некоторыми программами. Вы можете продолжить, однако правильная работа OpenPods не гарантируется.</string>
|
||||
<string name="miui_warning_continue">Продолжить</string>
|
||||
|
||||
<string name="noBtText">Похоже, что Bluetooth LE не поддерживается на Вашем устройстве</string>
|
||||
<string name="noBtText">Похоже, что Bluetooth LE не поддерживается на Вашем устройстве.</string>
|
||||
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="batterySaver">Режим энергосбережения (Не рекомендуется)</string>
|
||||
<string name="batterySaver">Режим энергосбережения (не рекомендуется)</string>
|
||||
<string name="batterySaver_desc">Включите, если Bluetooth использует слишком много ресурсов</string>
|
||||
<string name="restart" tools:ignore="MissingTranslation">Перезапуск службы</string>
|
||||
<string name="restart_desc" tools:ignore="MissingTranslation">На случай если уведомление не показывается</string>
|
||||
|
||||
<string name="about">О приложении</string>
|
||||
<string name="about_dev">Разработано Federico Dossena и Itai Levin</string>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="airpods1">• Apple AirPods 1st gen</string>
|
||||
<string name="airpods2">• Apple AirPods 2nd gen</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
@@ -30,4 +31,5 @@
|
||||
<string name="about_dev">Розроблено Federico Dossena й Itai Levin</string>
|
||||
<string name="website">Сайт</string>
|
||||
<string name="donate">Підтримати</string>
|
||||
<string name="airpodspro2">• Apple AirPods Pro 2</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="airpods1">• Apple AirPods 一代</string>
|
||||
<string name="airpods2">• Apple AirPods 二代</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
@@ -26,4 +27,5 @@
|
||||
<string name="about_dev">由 Federico Dossena 和 Itai Levin 开发</string>
|
||||
<string name="website">网站</string>
|
||||
<string name="donate">捐赠</string>
|
||||
<string name="airpodspro2">• Apple AirPods Pro 2</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="bg_noti_channel" translatable="false">Background Notification</string>
|
||||
<string name="bg_noti_title">背景運作中</string>
|
||||
<string name="bg_noti_text">點選以開啟通知設定</string>
|
||||
|
||||
<string name="intro_title">此應用讓您在任何 Android 裝置上檢查您的 AirPods 狀態</string>
|
||||
<string name="intro_message">為了正常運作,我們需要您開放藍牙低功耗(位置)以及背景運作的權限</string>
|
||||
<string name="intro_allow">允許</string>
|
||||
<string name="intro_step">步驟</string>
|
||||
<string name="intro_bt_perm">藍牙權限</string>
|
||||
<string name="intro_bat_perm">電池最佳化</string>
|
||||
<string name="intro_loc1_perm">位置權限</string>
|
||||
<string name="intro_loc2_perm">背景位置權限</string>
|
||||
<string name="intro_notif_perm">通知權限</string>
|
||||
<string name="msg_bt_perm">藍牙權限尚未開啟,請至設定中啟用。</string>
|
||||
<string name="msg_loc1_perm">位置權限尚未開啟,請至設定中啟用。</string>
|
||||
<string name="msg_loc2_perm">背景位置權限尚未開啟,請至設定中啟用。</string>
|
||||
<string name="msg_notif_perm">通知權限尚未開啟,請至設定中啟用。</string>
|
||||
|
||||
<string name="main_description">當您的 AirPods 連接時,此應用會顯示一個通知</string>
|
||||
<string name="supportedDevicesTitle"><u>支援的裝置:</u></string>
|
||||
<string name="airpods1">• Apple AirPods 第一代</string>
|
||||
<string name="airpods2">• Apple AirPods 第二代</string>
|
||||
<string name="airpods3">• Apple AirPods 第三代</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro2">• Apple AirPods Pro 2</string>
|
||||
<string name="airpodspro3">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
<string name="beatssolo3">• Beats Solo 3</string>
|
||||
<string name="beatsstudio3">• Beats Studio 3</string>
|
||||
<string name="powerbeats3">• Powerbeats 3</string>
|
||||
<string name="powerbeatspro">• Powerbeats Pro</string>
|
||||
|
||||
<string name="miui_warning">偵測到 MIUI</string>
|
||||
<string name="miui_warning_desc">此裝置使用的 ROM 存在已知的相容性問題。您可以繼續使用,但 OpenPods 可能無法正常運作。</string>
|
||||
<string name="miui_warning_continue">繼續</string>
|
||||
|
||||
<string name="noBtText">此裝置似乎沒有藍牙低功耗功能</string>
|
||||
|
||||
<string name="settings">設定</string>
|
||||
<string name="batterySaver">省電模式(不建議)</string>
|
||||
<string name="batterySaver_desc">如果藍牙使用過多電量,請啟用此功能</string>
|
||||
<string name="restart">重新啟動服務</string>
|
||||
<string name="restart_desc">如果通知未正常顯示,請執行此操作</string>
|
||||
|
||||
<string name="about">關於</string>
|
||||
<string name="about_dev">由 Federico Dossena 和 Itai Levin 開發</string>
|
||||
<string name="website">網站</string>
|
||||
<string name="donate">贊助</string>
|
||||
|
||||
</resources>
|
||||
@@ -2,7 +2,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name" translatable="false">OpenPods</string>
|
||||
|
||||
<string name="bg_noti_channel" translatable="false">Background Notification</string>
|
||||
<string name="bg_noti_channel" tools:ignore="MissingTranslation">Background Notification</string>
|
||||
<string name="bg_noti_title" tools:ignore="MissingTranslation">Running in the background</string>
|
||||
<string name="bg_noti_text" tools:ignore="MissingTranslation">Click to open notification settings</string>
|
||||
|
||||
@@ -14,16 +14,20 @@
|
||||
<string name="intro_bat_perm" tools:ignore="MissingTranslation">Battery Optimization</string>
|
||||
<string name="intro_loc1_perm" tools:ignore="MissingTranslation">Location Permission</string>
|
||||
<string name="intro_loc2_perm" tools:ignore="MissingTranslation">Background Location Permission</string>
|
||||
<string name="intro_notif_perm" tools:ignore="MissingTranslation">Notification Permission</string>
|
||||
<string name="msg_bt_perm" tools:ignore="MissingTranslation">Bluetooth permissions not granted, please enable them in settings.</string>
|
||||
<string name="msg_loc1_perm" tools:ignore="MissingTranslation">Location permission not granted, please enable it in settings.</string>
|
||||
<string name="msg_loc2_perm" tools:ignore="MissingTranslation">Background Location permission not granted, please enable it in settings.</string>
|
||||
<string name="msg_notif_perm" tools:ignore="MissingTranslation">Notification Permission not granted, please enable it in settings.</string>
|
||||
|
||||
<string name="main_description">The app will show a notification when your AirPods are connected</string>
|
||||
<string name="supportedDevicesTitle"><u>Supported devices:</u></string>
|
||||
<string name="airpods1">• Apple AirPods 1st gen</string>
|
||||
<string name="airpods2">• Apple AirPods 2nd gen</string>
|
||||
<string name="airpods3">• Apple AirPods 3rd gen</string>
|
||||
<string name="airpods3" tools:ignore="MissingTranslation">• Apple AirPods 3rd gen</string>
|
||||
<string name="airpodspro">• Apple AirPods Pro</string>
|
||||
<string name="airpodspro2" tools:ignore="MissingTranslation">• Apple AirPods Pro 2</string>
|
||||
<string name="airpodspro3" tools:ignore="MissingTranslation">• Apple AirPods Pro 3</string>
|
||||
<string name="airpodsmax">• Apple AirPods Max</string>
|
||||
<string name="beatsx">• Beats X</string>
|
||||
<string name="beatsflex">• Beats Flex</string>
|
||||
@@ -48,7 +52,7 @@
|
||||
<string name="about_dev">Developed by Federico Dossena and Itai Levin</string>
|
||||
<string name="fdroid" translatable="false">F-Droid</string>
|
||||
<string name="website">Website</string>
|
||||
<string name="github" tools:ignore="MissingTranslation">Github</string>
|
||||
<string name="github" translatable="false">Github</string>
|
||||
<string name="donate">Donate</string>
|
||||
|
||||
<string name="fdroid_url" translatable="false">https://f-droid.org/packages/com.dosse.airpods/</string>
|
||||
|
||||
@@ -1,27 +1,4 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication) apply false
|
||||
}
|
||||
@@ -6,12 +6,17 @@
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
@@ -0,0 +1,12 @@
|
||||
[versions]
|
||||
agp = "8.13.0"
|
||||
appcompat = "1.6.1"
|
||||
preference = "1.2.1"
|
||||
|
||||
[libraries]
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#Wed May 18 00:26:39 IDT 2022
|
||||
#Thu Sep 29 21:38:30 IDT 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1 +1,23 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "OpenPods"
|
||||
include ':app'
|
||||
|
||||