diff --git a/CHANGELOG.md b/CHANGELOG.md index 059f7956..bc66f9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ###Changelog +####Version 0.5.1 +* Pebble: support taking screenshot from Pebble Time +* Fix broken "find lost device" which was broken in 0.5.0 + ####Version 0.5.0 * Mi Band: fix setting wear location * Pebble: experimental watchapp installation support for FW 3.x/Pebble Time diff --git a/README.md b/README.md index 1e936587..c226c409 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ need to create an account and transmit any of your data to the vendor's servers. [![Gadgetbridge on F-Droid](/Get_it_on_F-Droid.svg.png?raw=true "Download from F-Droid")](https://f-droid.org/repository/browse/?fdid=nodomain.freeyourgadget.gadgetbridge) +[List of changes](CHANGELOG.md) + ## Features (Pebble) * Incoming calls notification and display (caller, phone number) diff --git a/app/build.gradle b/app/build.gradle index 899f5c54..e088675d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "nodomain.freeyourgadget.gadgetbridge" minSdkVersion 19 targetSdkVersion 21 - versionCode 21 - versionName "0.5.0" + versionCode 22 + versionName "0.5.1" } buildTypes { release { @@ -31,9 +31,16 @@ android { // optional path to report (default will be lint-results.html in the builddir) htmlOutput file("$project.buildDir/reports/lint/lint.html") } + + testOptions { + unitTests.returnDefaultValues = true + } } dependencies { + testCompile 'junit:junit:4.12' + testCompile "org.mockito:mockito-core:1.9.5" + compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:21.0.3' compile 'com.android.support:support-v4:21.0.3' @@ -41,9 +48,6 @@ dependencies { compile 'org.slf4j:slf4j-api:1.7.7' compile 'com.github.PhilJay:MPAndroidChart:2.1.0' compile 'com.github.pfichtner:durationformatter:0.1.1' - - testCompile 'junit:junit:4.12' -// testCompile "org.mockito:mockito-core:1.9.5" } check.dependsOn 'findbugs', 'pmd', 'lint' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 957a6be6..06d5efd5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,10 @@ android:name="android.hardware.bluetooth_le" android:required="false" /> + + mServiceClass; - public GBDeviceService(Context context, Class serviceClass) { + public GBDeviceService(Context context) { mContext = context; - mServiceClass = serviceClass; + mServiceClass = DeviceCommunicationService.class; } - private Intent createIntent() { + protected Intent createIntent() { Intent startIntent = new Intent(mContext, mServiceClass); return startIntent; } @@ -174,7 +175,7 @@ public class GBDeviceService implements DeviceService { @Override public void onFindDevice(boolean start) { Intent intent = createIntent().setAction(ACTION_FIND_DEVICE) - .putExtra(EXTRA_APP_UUID, start); + .putExtra(EXTRA_FIND_START, start); invokeService(intent); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 1c4a906a..04d33fb7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -3,5 +3,5 @@ package nodomain.freeyourgadget.gadgetbridge.model; public enum DeviceType { UNKNOWN, PEBBLE, - MIBAND + TEST, MIBAND } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index c91200a7..7fa301c3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -13,6 +13,7 @@ import android.net.Uri; import android.os.IBinder; import android.preference.PreferenceManager; import android.provider.ContactsContract; +import android.support.annotation.Nullable; import android.support.v4.content.LocalBroadcastManager; import android.widget.Toast; @@ -27,13 +28,48 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; import nodomain.freeyourgadget.gadgetbridge.util.GB; -import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.*; + +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CALLSTATE; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DISCONNECT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FETCH_ACTIVITY_DATA; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FIND_DEVICE; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_INSTALL; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_NOTIFICATION_EMAIL; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_NOTIFICATION_GENERIC; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_NOTIFICATION_SMS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REBOOT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_APPINFO; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_DEVICEINFO; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_SCREENSHOT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICINFO; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_ALARMS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_UUID; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_COMMAND; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_PHONENUMBER; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_DEVICE_ADDRESS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACK; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SENDER; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SUBJECT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TITLE; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_PERFORM_PAIR; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_URI; public class DeviceCommunicationService extends Service { private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class); private boolean mStarted = false; + private DeviceSupportFactory mFactory; private GBDevice mGBDevice = null; private DeviceSupport mDeviceSupport; @@ -60,6 +96,7 @@ public class DeviceCommunicationService extends Service { LOG.debug("DeviceCommunicationService is being created"); super.onCreate(); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)); + mFactory = new DeviceSupportFactory(this); } @Override @@ -107,32 +144,29 @@ public class DeviceCommunicationService extends Service { start(); // ensure started String btDeviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS); SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - if (btDeviceAddress == null) { - btDeviceAddress = sharedPrefs.getString("last_device_address", null); - } else { - sharedPrefs.edit().putString("last_device_address", btDeviceAddress).apply(); + if (sharedPrefs != null) { // may be null in test cases + if (btDeviceAddress == null) { + btDeviceAddress = sharedPrefs.getString("last_device_address", null); + } else { + sharedPrefs.edit().putString("last_device_address", btDeviceAddress).apply(); + } } if (btDeviceAddress != null && !isConnecting() && !isConnected()) { - if (mDeviceSupport != null) { - mDeviceSupport.dispose(); - mDeviceSupport = null; - } + setDeviceSupport(null); try { - DeviceSupportFactory factory = new DeviceSupportFactory(this); - mDeviceSupport = factory.createDeviceSupport(btDeviceAddress); - if (mDeviceSupport != null) { - mGBDevice = mDeviceSupport.getDevice(); + DeviceSupport deviceSupport = mFactory.createDeviceSupport(btDeviceAddress); + if (deviceSupport != null) { + setDeviceSupport(deviceSupport); if (pair) { - mDeviceSupport.pair(); + deviceSupport.pair(); } else { - mDeviceSupport.connect(); + deviceSupport.connect(); } } } catch (Exception e) { GB.toast(this, getString(R.string.cannot_connect, e.getMessage()), Toast.LENGTH_SHORT, GB.ERROR); - mDeviceSupport = null; - mGBDevice = null; + setDeviceSupport(null); } } else if (mGBDevice != null) { // send an update at least @@ -231,6 +265,29 @@ public class DeviceCommunicationService extends Service { return START_STICKY; } + /** + * For testing! + * @param factory + */ + public void setDeviceSupportFactory(DeviceSupportFactory factory) { + mFactory = factory; + } + + /** + * Disposes the current DeviceSupport instance (if any) and sets a new device support instance + * (if not null). + * @param deviceSupport + */ + private void setDeviceSupport(@Nullable DeviceSupport deviceSupport) { + if (deviceSupport != mDeviceSupport && mDeviceSupport != null) { + mDeviceSupport.dispose(); + mDeviceSupport = null; + mGBDevice = null; + } + mDeviceSupport = deviceSupport; + mGBDevice = mDeviceSupport != null ? mDeviceSupport.getDevice() : null; + } + private void start() { if (!mStarted) { startForeground(GB.NOTIFICATION_ID, GB.createNotification(getString(R.string.gadgetbridge_running), this)); @@ -238,6 +295,10 @@ public class DeviceCommunicationService extends Service { } } + public boolean isStarted() { + return mStarted; + } + private boolean isConnected() { return mGBDevice != null && mGBDevice.isConnected(); } @@ -258,9 +319,7 @@ public class DeviceCommunicationService extends Service { LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); GB.setReceiversEnableState(false, this); // disable BroadcastReceivers - if (mDeviceSupport != null) { - mDeviceSupport.dispose(); - } + setDeviceSupport(null); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification wont be cancelled when service stops } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index bd3b1bfb..47530d2f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -5,6 +5,7 @@ import android.bluetooth.BluetoothDevice; import android.content.Context; import android.widget.Toast; +import java.lang.reflect.Constructor; import java.util.EnumSet; import nodomain.freeyourgadget.gadgetbridge.GBException; @@ -26,10 +27,17 @@ public class DeviceSupportFactory { public synchronized DeviceSupport createDeviceSupport(String deviceAddress) throws GBException { DeviceSupport deviceSupport; - if (deviceAddress.indexOf(":") == deviceAddress.lastIndexOf(":")) { // only one colon - deviceSupport = createTCPDeviceSupport(deviceAddress); + int indexFirstColon = deviceAddress.indexOf(":"); + if (indexFirstColon > 0) { + if (indexFirstColon == deviceAddress.lastIndexOf(":")) { // only one colon + deviceSupport = createTCPDeviceSupport(deviceAddress); + } else { + // multiple colons -- bt? + deviceSupport = createBTDeviceSupport(deviceAddress); + } } else { - deviceSupport = createBTDeviceSupport(deviceAddress); + // no colon at all, maybe a class name? + deviceSupport = createClassNameDeviceSupport(deviceAddress); } if (deviceSupport != null) { @@ -41,6 +49,21 @@ public class DeviceSupportFactory { return null; } + private DeviceSupport createClassNameDeviceSupport(String className) throws GBException { + try { + Class deviceSupportClass = Class.forName(className); + Constructor constructor = deviceSupportClass.getConstructor(); + DeviceSupport support = (DeviceSupport) constructor.newInstance(); + // has to create the device itself + support.setContext(null, null, mContext); + return support; + } catch (ClassNotFoundException e) { + return null; // not a class, or not known at least + } catch (Exception e) { + throw new GBException("Error creating DeviceSupport instance for " + className, e); + } + } + private void checkBtAvailability() { if (mBtAdapter == null) { GB.toast(mContext.getString(R.string.bluetooth_is_not_supported_), Toast.LENGTH_SHORT, GB.WARN); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 82329a9f..0a60245d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -202,6 +202,97 @@ public class PebbleProtocol extends GBDeviceProtocol { GBDeviceEventScreenshot mDevEventScreenshot = null; int mScreenshotRemaining = -1; + //monochrome black + white + static final byte[] clut_pebble = { + 0x00, 0x00, 0x00, 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00 + }; + + // linear BGR222 (6 bit, 64 entries) + static final byte[] clut_pebbletime = new byte[]{ + 0x00, 0x00, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, + (byte) 0xaa, 0x00, 0x00, 0x00, + (byte) 0xff, 0x00, 0x00, 0x00, + + 0x00, 0x55, 0x00, 0x00, + 0x55, 0x55, 0x00, 0x00, + (byte) 0xaa, 0x55, 0x00, 0x00, + (byte) 0xff, 0x55, 0x00, 0x00, + + 0x00, (byte) 0xaa, 0x00, 0x00, + 0x55, (byte) 0xaa, 0x00, 0x00, + (byte) 0xaa, (byte) 0xaa, 0x00, 0x00, + (byte) 0xff, (byte) 0xaa, 0x00, 0x00, + + 0x00, (byte) 0xff, 0x00, 0x00, + 0x55, (byte) 0xff, 0x00, 0x00, + (byte) 0xaa, (byte) 0xff, 0x00, 0x00, + (byte) 0xff, (byte) 0xff, 0x00, 0x00, + + 0x00, 0x00, 0x55, 0x00, + 0x55, 0x00, 0x55, 0x00, + (byte) 0xaa, 0x00, 0x55, 0x00, + (byte) 0xff, 0x00, 0x55, 0x00, + + 0x00, 0x55, 0x55, 0x00, + 0x55, 0x55, 0x55, 0x00, + (byte) 0xaa, 0x55, 0x55, 0x00, + (byte) 0xff, 0x55, 0x55, 0x00, + + 0x00, (byte) 0xaa, 0x55, 0x00, + 0x55, (byte) 0xaa, 0x55, 0x00, + (byte) 0xaa, (byte) 0xaa, 0x55, 0x00, + (byte) 0xff, (byte) 0xaa, 0x55, 0x00, + + 0x00, (byte) 0xff, 0x55, 0x00, + 0x55, (byte) 0xff, 0x55, 0x00, + (byte) 0xaa, (byte) 0xff, 0x55, 0x00, + (byte) 0xff, (byte) 0xff, 0x55, 0x00, + + 0x00, 0x00, (byte) 0xaa, 0x00, + 0x55, 0x00, (byte) 0xaa, 0x00, + (byte) 0xaa, 0x00, (byte) 0xaa, 0x00, + (byte) 0xff, 0x00, (byte) 0xaa, 0x00, + + 0x00, 0x55, (byte) 0xaa, 0x00, + 0x55, 0x55, (byte) 0xaa, 0x00, + (byte) 0xaa, 0x55, (byte) 0xaa, 0x00, + (byte) 0xff, 0x55, (byte) 0xaa, 0x00, + + 0x00, (byte) 0xaa, (byte) 0xaa, 0x00, + 0x55, (byte) 0xaa, (byte) 0xaa, 0x00, + (byte) 0xaa, (byte) 0xaa, (byte) 0xaa, 0x00, + (byte) 0xff, (byte) 0xaa, (byte) 0xaa, 0x00, + + 0x00, (byte) 0xff, (byte) 0xaa, 0x00, + 0x55, (byte) 0xff, (byte) 0xaa, 0x00, + (byte) 0xaa, (byte) 0xff, (byte) 0xaa, 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xaa, 0x00, + + 0x00, 0x00, (byte) 0xff, 0x00, + 0x55, 0x00, (byte) 0xff, 0x00, + (byte) 0xaa, 0x00, (byte) 0xff, 0x00, + (byte) 0xff, 0x00, (byte) 0xff, 0x00, + + 0x00, 0x55, (byte) 0xff, 0x00, + 0x55, 0x55, (byte) 0xff, 0x00, + (byte) 0xaa, 0x55, (byte) 0xff, 0x00, + (byte) 0xff, 0x55, (byte) 0xff, 0x00, + + 0x00, (byte) 0xaa, (byte) 0xff, 0x00, + 0x55, (byte) 0xaa, (byte) 0xff, 0x00, + (byte) 0xaa, (byte) 0xaa, (byte) 0xff, 0x00, + (byte) 0xff, (byte) 0xaa, (byte) 0xff, 0x00, + + 0x00, (byte) 0xff, (byte) 0xff, 0x00, + 0x55, (byte) 0xff, (byte) 0xff, 0x00, + (byte) 0xaa, (byte) 0xff, (byte) 0xff, 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, + + }; + + byte last_id = -1; private ArrayList tmpUUIDS = new ArrayList<>(); @@ -921,34 +1012,38 @@ public class PebbleProtocol extends GBDeviceProtocol { byte result = buf.get(); mDevEventScreenshot = new GBDeviceEventScreenshot(); int version = buf.getInt(); - if (result != 0 || version != 1) { // pebble time not yet + if (result != 0) { return null; } mDevEventScreenshot.width = buf.getInt(); mDevEventScreenshot.height = buf.getInt(); - mDevEventScreenshot.bpp = 1; - mDevEventScreenshot.clut = new byte[]{ - 0x00, 0x00, 0x00, 0x00, (byte) 0xff, - (byte) 0xff, (byte) 0xff, 0x00 - }; - mScreenshotRemaining = (mDevEventScreenshot.width * mDevEventScreenshot.height) / 8; - if (mScreenshotRemaining > 50000) { - mScreenshotRemaining = -1; // ignore too big values - return null; + if (version == 1) { + mDevEventScreenshot.bpp = 1; + mDevEventScreenshot.clut = clut_pebble; + } else { + mDevEventScreenshot.bpp = 8; + mDevEventScreenshot.clut = clut_pebbletime; } + + mScreenshotRemaining = (mDevEventScreenshot.width * mDevEventScreenshot.height * mDevEventScreenshot.bpp) / 8; + mDevEventScreenshot.data = new byte[mScreenshotRemaining]; length -= 13; } if (mScreenshotRemaining == -1) { return null; } - for (int i = 0; i < length; i++) { - byte corrected = reverseBits(buf.get()); + byte corrected = buf.get(); + if (mDevEventScreenshot.bpp == 1) { + corrected = reverseBits(corrected); + } else { + corrected = (byte) (corrected & 0b00111111); + } + mDevEventScreenshot.data[mDevEventScreenshot.data.length - mScreenshotRemaining + i] = corrected; } - mScreenshotRemaining -= length; LOG.info("Screenshot remaining bytes " + mScreenshotRemaining); if (mScreenshotRemaining == 0) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java index d96dbf4a..fb73f9b0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -24,6 +24,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.GBEnvironment; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; @@ -43,6 +44,7 @@ public class GB { public static final int INFO = 1; public static final int WARN = 2; public static final int ERROR = 3; + public static GBEnvironment environment; public static Notification createNotification(String text, Context context) { Intent notificationIntent = new Intent(context, ControlCenter.class); @@ -152,16 +154,16 @@ public class GB { headerbuf.putInt(screenshot.width); headerbuf.putInt(-screenshot.height); headerbuf.putShort((short) 1); // planes - headerbuf.putShort((short) 1); // bit count + headerbuf.putShort((short) screenshot.bpp); headerbuf.putInt(0); // compression headerbuf.putInt(0); // length of pixeldata in byte (uncompressed=0) headerbuf.putInt(0); // pixels per meter (x) headerbuf.putInt(0); // pixels per meter (y) - headerbuf.putInt(2); // number of colors in CLUT - headerbuf.putInt(2); // numbers of used colors + headerbuf.putInt(screenshot.clut.length / 4); // number of colors in CLUT + headerbuf.putInt(0); // numbers of used colors headerbuf.put(screenshot.clut); fos.write(headerbuf.array()); - int rowbytes = screenshot.width / 8; + int rowbytes = (screenshot.width * screenshot.bpp) / 8; byte[] pad = new byte[rowbytes % 4]; for (int i = 0; i < screenshot.height; i++) { fos.write(screenshot.data, rowbytes * i, rowbytes); @@ -225,6 +227,9 @@ public class GB { * @param ex optional exception to be logged */ public static void toast(final Context context, final String message, final int displayTime, final int severity, final Throwable ex) { + if (env().isLocalTest()) { + return; + } Looper mainLooper = Looper.getMainLooper(); if (Thread.currentThread() == mainLooper.getThread()) { log(message, severity, ex); @@ -321,4 +326,8 @@ public class GB { NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(NOTIFICATION_ID_LOW_BATTERY, notification); } + + public static GBEnvironment env() { + return environment; + } } diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractServiceTestCase.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractServiceTestCase.java new file mode 100644 index 00000000..ea82165d --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractServiceTestCase.java @@ -0,0 +1,97 @@ +package nodomain.freeyourgadget.gadgetbridge.service; + +import android.app.Application; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; + +import junit.framework.Assert; + +import org.junit.After; +import org.junit.Before; + +import nodomain.freeyourgadget.gadgetbridge.test.GBMockApplication; +import nodomain.freeyourgadget.gadgetbridge.test.GBMockContext; +import nodomain.freeyourgadget.gadgetbridge.test.GBMockPackageManager; +import nodomain.freeyourgadget.gadgetbridge.test.MockHelper; + +public abstract class AbstractServiceTestCase { + private static final int ID = -1; // currently not supported + private Class mServiceClass; + private T mServiceInstance; + private Context mContext; + private Application mApplication; + private boolean wasStarted; + private PackageManager mPackageManager; + private NotificationManager mNotificationManager; + private MockHelper mMockHelper; + + protected AbstractServiceTestCase(Class serviceClass) { + mServiceClass = serviceClass; + Assert.assertNotNull(serviceClass); + } + + public Context getContext() { + return mContext; + } + + public T getServiceInstance() { + return mServiceInstance; + } + + @Before + public void setUp() throws Exception { + mMockHelper = new MockHelper(); + mPackageManager = createPackageManager(); + mApplication = createApplication(mPackageManager); + mContext = createContext(mApplication); + mNotificationManager = mMockHelper.createNotificationManager(mContext); + mServiceInstance = createService(mServiceClass, mApplication, mNotificationManager); + mServiceInstance.onCreate(); + } + + @After + public void tearDown() throws Exception { + if (mServiceInstance != null) { + stopService(); + } + } + + public void startService(Intent intent) { + wasStarted = true; + mServiceInstance.onStartCommand(intent, Service.START_FLAG_REDELIVERY, ID); + } + + public void stopService() { + mServiceInstance.onDestroy(); + mServiceInstance = null; + } + + protected Application createApplication(PackageManager packageManager) { + return new GBMockApplication(packageManager); + } + + protected PackageManager createPackageManager() { + return new GBMockPackageManager(); + } + + protected Application getApplication() { + return mApplication; + } + + protected Context createContext(final Application application) { + return new GBMockContext(application); + } + + private T createService(Class serviceClass, Application application, NotificationManager notificationManager) throws Exception { + T service = mMockHelper.createService(serviceClass, application); + mMockHelper.addSystemServiceTo(service, Context.NOTIFICATION_SERVICE, getNotificationService()); + return service; + } + + private NotificationManager getNotificationService() { + return mNotificationManager; + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationServiceTestCase.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationServiceTestCase.java new file mode 100644 index 00000000..f32f6f9b --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationServiceTestCase.java @@ -0,0 +1,82 @@ +package nodomain.freeyourgadget.gadgetbridge.service; + +import android.content.Context; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class DeviceCommunicationServiceTestCase extends AbstractServiceTestCase { + private static final java.lang.String TEST_DEVICE_ADDRESS = TestDeviceSupport.class.getName(); + + /** + * Factory that always returns the mockSupport instance + */ + private class TestDeviceSupportFactory extends DeviceSupportFactory { + public TestDeviceSupportFactory(Context context) { + super(context); + } + + @Override + public synchronized DeviceSupport createDeviceSupport(String deviceAddress) throws GBException { + return mockSupport; + } + } + + private TestDeviceService mDeviceService; + @Mock + private TestDeviceSupport realSupport; + private TestDeviceSupport mockSupport; + + public DeviceCommunicationServiceTestCase() { + super(DeviceCommunicationService.class); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + mockSupport = null; + realSupport = new TestDeviceSupport(); + realSupport.setContext(new GBDevice(TEST_DEVICE_ADDRESS, "Test Device", DeviceType.TEST), null, getContext()); + mockSupport = Mockito.spy(realSupport); + getServiceInstance().setDeviceSupportFactory(new TestDeviceSupportFactory(getContext())); + + mDeviceService = new TestDeviceService(this); + } + + @Test + public void testStart() { + assertFalse("Service was already", getServiceInstance().isStarted()); + mDeviceService.start(); + assertTrue("Service should be started", getServiceInstance().isStarted()); + } + + @Test + public void ensureConnected() { + mDeviceService.connect(TEST_DEVICE_ADDRESS); + Mockito.verify(mockSupport, Mockito.times(1)).connect(); + assertTrue(realSupport.getDevice().isInitialized()); + } + + @Test + public void testFindDevice() { + ensureConnected(); + + InOrder inOrder = Mockito.inOrder(mockSupport); + mDeviceService.onFindDevice(true); + mDeviceService.onFindDevice(false); + inOrder.verify(mockSupport, Mockito.times(1)).onFindDevice(true); + inOrder.verify(mockSupport, Mockito.times(1)).onFindDevice(false); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceService.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceService.java new file mode 100644 index 00000000..ad4a11be --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceService.java @@ -0,0 +1,25 @@ +package nodomain.freeyourgadget.gadgetbridge.service; + +import android.content.Intent; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService; +import nodomain.freeyourgadget.gadgetbridge.test.GBMockIntent; + +public class TestDeviceService extends GBDeviceService { + private final AbstractServiceTestCase mTestCase; + + public TestDeviceService(AbstractServiceTestCase testCase) throws Exception { + super(testCase.getContext()); + mTestCase = testCase; + } + + @Override + protected Intent createIntent() { + return new GBMockIntent(); + } + + @Override + protected void invokeService(Intent intent) { + mTestCase.startService(intent); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java new file mode 100644 index 00000000..61de0b47 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java @@ -0,0 +1,124 @@ +package nodomain.freeyourgadget.gadgetbridge.service; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.net.Uri; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; +import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; + +public class TestDeviceSupport extends AbstractDeviceSupport { + + public TestDeviceSupport() { + } + + @Override + public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) { + gbDevice = new GBDevice(getClass().getName(), "Test Device", DeviceType.TEST); + super.setContext(gbDevice, btAdapter, context); + } + + @Override + public boolean connect() { + gbDevice.setState(GBDevice.State.INITIALIZED); + gbDevice.sendDeviceUpdateIntent(getContext()); + return true; + } + + @Override + public void dispose() { + + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + public void pair() { + + } + + @Override + public void onSMS(String from, String body) { + + } + + @Override + public void onEmail(String from, String subject, String body) { + + } + + @Override + public void onGenericNotification(String title, String details) { + + } + + @Override + public void onSetTime() { + + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onSetCallState(@Nullable String number, @Nullable String name, ServiceCommand command) { + + } + + @Override + public void onSetMusicInfo(String artist, String album, String track) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onFetchActivityData() { + + } + + @Override + public void onReboot() { + + } + + @Override + public void onFindDevice(boolean start) { + + } + + @Override + public void onScreenshotReq() { + + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockApplication.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockApplication.java new file mode 100644 index 00000000..b9f16b12 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockApplication.java @@ -0,0 +1,27 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.test.mock.MockApplication; + +import nodomain.freeyourgadget.gadgetbridge.GBEnvironment; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class GBMockApplication extends MockApplication { + private final PackageManager mPackageManager; + + public GBMockApplication(PackageManager packageManager) { + GB.environment = GBEnvironment.createDeviceEnvironment().createLocalTestEnvironment(); + mPackageManager = packageManager; + } + + @Override + public Context getApplicationContext() { + return this; + } + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockContext.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockContext.java new file mode 100644 index 00000000..ce7d5332 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockContext.java @@ -0,0 +1,24 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import android.app.Application; +import android.content.Context; +import android.content.pm.PackageManager; +import android.test.mock.MockContext; + +public class GBMockContext extends MockContext { + private final Application mApplication; + + public GBMockContext(Application application) { + mApplication = application; + } + + @Override + public Context getApplicationContext() { + return mApplication; + } + + @Override + public PackageManager getPackageManager() { + return mApplication.getPackageManager(); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockIntent.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockIntent.java new file mode 100644 index 00000000..a6bb9857 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockIntent.java @@ -0,0 +1,387 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class GBMockIntent extends Intent { + private String mAction; + private Map extras = new HashMap<>(); + + @NonNull + @Override + public Intent setAction(String action) { + mAction = action; + return this; + } + + @Override + public String getAction() { + return mAction; + } + + @NonNull + @Override + public Intent putExtra(String name, boolean value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, byte value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, char value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, short value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, int value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, long value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, float value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, double value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, String value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, CharSequence value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, Parcelable value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, Parcelable[] value) { + extras.put(name, value); + return this; + } + + @Override + public Intent putParcelableArrayListExtra(String name, ArrayList value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putIntegerArrayListExtra(String name, ArrayList value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putStringArrayListExtra(String name, ArrayList value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putCharSequenceArrayListExtra(String name, ArrayList value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, Serializable value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, boolean[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, byte[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, short[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, char[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, int[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, long[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, float[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, double[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, String[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, CharSequence[] value) { + extras.put(name, value); + return this; + } + + @NonNull + @Override + public Intent putExtra(String name, Bundle value) { + extras.put(name, value); + return this; + } + + @Override + public boolean getBooleanExtra(String name, boolean defaultValue) { + if (extras.containsKey(name)) { + return (boolean) extras.get(name); + } + return defaultValue; + } + + @Override + public byte getByteExtra(String name, byte defaultValue) { + if (extras.containsKey(name)) { + return (byte) extras.get(name); + } + return defaultValue; + } + + @Override + public short getShortExtra(String name, short defaultValue) { + if (extras.containsKey(name)) { + return (short) extras.get(name); + } + return defaultValue; + } + + @Override + public char getCharExtra(String name, char defaultValue) { + if (extras.containsKey(name)) { + return (char) extras.get(name); + } + return defaultValue; + } + + @Override + public int getIntExtra(String name, int defaultValue) { + if (extras.containsKey(name)) { + return (int) extras.get(name); + } + return defaultValue; + } + + @Override + public long getLongExtra(String name, long defaultValue) { + if (extras.containsKey(name)) { + return (long) extras.get(name); + } + return defaultValue; + } + + @Override + public float getFloatExtra(String name, float defaultValue) { + if (extras.containsKey(name)) { + return (float) extras.get(name); + } + return defaultValue; + } + + @Override + public double getDoubleExtra(String name, double defaultValue) { + if (extras.containsKey(name)) { + return (double) extras.get(name); + } + return defaultValue; + } + + @Override + public CharSequence getCharSequenceExtra(String name) { + return (CharSequence) extras.get(name); + } + + @Override + public T getParcelableExtra(String name) { + return (T) extras.get(name); + } + + @Override + public Parcelable[] getParcelableArrayExtra(String name) { + return (Parcelable[]) extras.get(name); + } + + @Override + public ArrayList getParcelableArrayListExtra(String name) { + return (ArrayList) extras.get(name); + } + + @Override + public Serializable getSerializableExtra(String name) { + return (Serializable) extras.get(name); + } + + @Override + public ArrayList getIntegerArrayListExtra(String name) { + return (ArrayList) extras.get(name); + } + + @Override + public ArrayList getStringArrayListExtra(String name) { + return (ArrayList) extras.get(name); + } + + @Override + public ArrayList getCharSequenceArrayListExtra(String name) { + return (ArrayList) extras.get(name); + } + + @Override + public boolean[] getBooleanArrayExtra(String name) { + return (boolean[]) extras.get(name); + } + + @Override + public byte[] getByteArrayExtra(String name) { + return (byte[]) extras.get(name); + } + + @Override + public short[] getShortArrayExtra(String name) { + return (short[]) extras.get(name); + } + + @Override + public char[] getCharArrayExtra(String name) { + return (char[]) extras.get(name); + } + + @Override + public int[] getIntArrayExtra(String name) { + return (int[]) extras.get(name); + } + + @Override + public long[] getLongArrayExtra(String name) { + return (long[]) extras.get(name); + } + + @Override + public float[] getFloatArrayExtra(String name) { + return (float[]) extras.get(name); + } + + @Override + public double[] getDoubleArrayExtra(String name) { + return (double[]) extras.get(name); + } + + @Override + public String[] getStringArrayExtra(String name) { + return (String[]) extras.get(name); + } + + @Override + public CharSequence[] getCharSequenceArrayExtra(String name) { + return (CharSequence[]) extras.get(name); + } + + @Override + public String getStringExtra(String name) { + return (String) extras.get(name); + } + + @Override + public String toString() { + return "GBMockIntent: " + mAction; + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockPackageManager.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockPackageManager.java new file mode 100644 index 00000000..7799532d --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBMockPackageManager.java @@ -0,0 +1,11 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import android.content.ComponentName; +import android.test.mock.MockPackageManager; + +public class GBMockPackageManager extends MockPackageManager { + @Override + public void setComponentEnabledSetting(ComponentName componentName, int newState, int flags) { + // do nothing + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/MockHelper.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/MockHelper.java new file mode 100644 index 00000000..9461cab1 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/MockHelper.java @@ -0,0 +1,35 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import android.app.Application; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; + +import junit.framework.Assert; + +import org.mockito.Mockito; + +import java.lang.reflect.Constructor; + +public class MockHelper { + public NotificationManager createNotificationManager(Context mContext) throws Exception { + Constructor[] constructors = NotificationManager.class.getDeclaredConstructors(); + constructors[0].setAccessible(true); + Class[] parameterTypes = constructors[0].getParameterTypes(); + return (NotificationManager) constructors[0].newInstance(); + } + + public T createService(Class serviceClass, Application application) throws Exception { + Constructor constructor = serviceClass.getConstructor(); + Assert.assertNotNull(constructor); + T realService = constructor.newInstance(); + T mockedService = Mockito.spy(realService); + Mockito.when(mockedService.getApplicationContext()).thenReturn(application); + Mockito.when(mockedService.getPackageManager()).thenReturn(application.getPackageManager()); + return mockedService; + } + + public void addSystemServiceTo(Context context, String serviceName, Object service) { + Mockito.when(context.getSystemService(serviceName)).thenReturn(service); + } +}