diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db1555f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/dictionaries/*.xml +/.idea/libraries +.DS_Store +/build +/key* \ No newline at end of file diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..9f676f0 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +nrf-beacon-nearby \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0c7e4a6 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1a3eaff --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ffeca08 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ef2d27 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Android-nRF-Beacon-for-Eddystone +nRF Beacon for Eddystone is an application that supports the new Eddystone GATT configuration service allowing users to configure your beacon to advertise all types of Eddystone frame types from UID, URL, TLM and the newest EID and eTLM frame types. In addition the application uses Nearby API for scanning Eddystone beacons in close proximity and Google Proximity API to register UID, EID beacons and create attachments for them on the Proximity API cloud. + +## Features +The basic features of the application includes + +-Foreground and background scanning of beacons using Nearby API which are registered on the Proximity API. Once an EID beacon is registered to the cloud with an attachment, the Nearby API will send the beacon EID packet to the proximity API, resolves the EID packet and retrieves the data attached to it. + Please note only UID and EID beacon types can be registered on the proximity API + +-Registering beacons and creating attachments to proximity API + +-Configuration of Eddystone beacons using the new Eddystone GATT configuration service. + +-URL shortener for configuring URL beacons + +## How to Guide +1. First press the button 1 on the nRF52 Devkit which turns the devkit/beacon in to connectable mode for 60 seconds +2. Press the connect button on the update tab on the application and the list of devices will be prompted. +3. Select the device and you will be challenged with the beacon manufacturer lock code. the lock code used for this application is 16 F's and the application will have the lock code hard coded +4. After entering the correct lock code the application will read through all slots and display the information for each slot. + +This application goes hand in hand with the nRF5 SDK for Eddystone posted on Github on the following link. This link also contain a complete how to guide and the new Eddystone GATT configuration specs. Please note that some of the advanced characteristics are not supported on the firmware application and will be implemented in the near future. + +https://github.com/NordicSemiconductor/nrf5-sdk-for-eddystone + +The nRF Beacon for Eddystone application is available on Playstore on the following link. + +https://play.google.com/store/apps/details?id=no.nordicsemi.android.nrfbeacon.nearby + +Note: + +-Android 4.3 or newer is required. + +-Tested on Samsung S3 with Android 4.3, on Nexus 5, 6 and 9 with lollipop & Marshmallow and Samsung Galaxy S6, S7 with Marshmallow. + +-Location Services need to be enabled for scanning on android 6.0 Marshmallow requesting a runtime persmission ACCESS_COARSE_LOCATION + +-GET_ACCOUNTS permission is required in order to select the account to register and allow access to the Google Proximity API and URL Shortener API. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..4fedb83 --- /dev/null +++ b/app/app.iml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..b68e8b1 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + defaultConfig { + applicationId "no.nordicsemi.android.nrfbeacon.nearby" + minSdkVersion 18 + targetSdkVersion 23 + versionCode 1 + versionName '1.0' + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + productFlavors { + } +} + +repositories { + flatDir { + dirs 'libs' + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:23.3.0' + compile 'com.android.support:design:23.3.0' + compile 'no.nordicsemi.android.support.v18:scanner:0.1.1' + compile 'com.google.android.gms:play-services-nearby:8.4.0' + compile 'com.google.android.gms:play-services-location:8.4.0' + compile 'com.squareup.okhttp:okhttp:2.4.0' + compile project(':libproximitybeacon') + compile project(':libeddystoneeidr') +} diff --git a/app/libs/nrf-beacon-lib-v2.0.aar b/app/libs/nrf-beacon-lib-v2.0.aar new file mode 100644 index 0000000..c38b231 Binary files /dev/null and b/app/libs/nrf-beacon-lib-v2.0.aar differ diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000..0ce66c0 --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/app/sources/nrf-beacon-lib-v2.0.jar b/app/sources/nrf-beacon-lib-v2.0.jar new file mode 100644 index 0000000..e0d24e9 Binary files /dev/null and b/app/sources/nrf-beacon-lib-v2.0.jar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cf8aa9a --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/fonts/trebuc.ttf b/app/src/main/assets/fonts/trebuc.ttf new file mode 100644 index 0000000..8489198 Binary files /dev/null and b/app/src/main/assets/fonts/trebuc.ttf differ diff --git a/app/src/main/assets/fonts/trebucbd.ttf b/app/src/main/assets/fonts/trebucbd.ttf new file mode 100644 index 0000000..663946d Binary files /dev/null and b/app/src/main/assets/fonts/trebucbd.ttf differ diff --git a/app/src/main/java/eidr/Curve25519.java b/app/src/main/java/eidr/Curve25519.java new file mode 100644 index 0000000..1297a9a --- /dev/null +++ b/app/src/main/java/eidr/Curve25519.java @@ -0,0 +1,798 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package eidr; + +import java.util.Arrays; + +/** + * Class providing the primitives of Curve25519; in particular, the ability of retrieving the x + * coordinate of nQ given n and the x coordinate of a point Q on the curve, and the special case + * where Q is the base point of Curve25519. + */ +class Curve25519 { + private static final byte[] BASE_POINT = new byte[]{ + 9, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + }; + + /** + * Modifies {@code source} to a Curve25519 private key. + */ + /* @VisibleForTesting */ static void toPrivateKey(byte[] source) { + if (source.length != 32) { + throw new IllegalArgumentException("All keys must be exactly 32 bytes long."); + } + source[0] &= 248; + source[31] &= 127; + source[31] |= 64; + } + + /** + * Returns the x coordinates of the point nQ, where Q is the base point. + * + *

Used to generate a public key from a secret key. + */ + public static byte[] scalarMultBase(byte[] n) { + toPrivateKey(n); + return scalarMult(n, BASE_POINT); + } + + /** + * Returns the x coordinates of the point nQ, where Q is a point with x coordinate equal to q. + * + *

Used to generate a shared secret between the owner of the (secret) n and the owner of the + * secret paired with q. + */ + public static byte[] scalarMult(byte[] n, byte[] q) { + if (n.length != 32 || q.length != 32) { + throw new IllegalArgumentException("All keys must be exactly 32 bytes long."); + } + toPrivateKey(n); + Polynomial25519 qPolynomial = new Polynomial25519(q); + Polynomial25519.Montgomery m = Polynomial25519.multiple(n, qPolynomial); + Polynomial25519 reciprocal = m.z.reciprocal(); + reciprocal.mult(m.x); + return reciprocal.toBytes(); + } + + /** + * An element of F_{2^255 - 19} in its polynomial form: given an object defined by an array c, the + * corresponding polynomial is sum(2^(ceil(25.5 * i) * c[i] * x^i), and the element is the + * valuation of the polynomial at 1. + * + *

The "reduced degree" form has degree less than 10; the extended form has degree less than + * 19, and is used to handle the output of the multiplication of these elements. Some operations + * assume to act on reduced degree polynomials. + * + *

The "reduced coefficients" form has coefficients less than 2^26 in absolute value. The + * extended coefficient form has coefficient less than 2^62 in absolute value. Some operations + * assume to act on reduced coefficients polynomials. + * + *

It is a responsibility of the caller to make sure to use the correct form whenever + * necessary. + * + *

See the paper [1] Curve25519: new Diffie-Hellman speed records at + * http://cr.yp.to/ecdh/curve25519-20060209.pdf for more information + */ + /* @VisibleForTesting */ static class Polynomial25519 { + private long[] c = new long[19]; + + /** + * An element of F_{2^255-19} in the "Montgomery" form, i.e. the element corresponding to (x, z) + * is x/z = x * z^{-1}. + */ + private static class Montgomery { + public Polynomial25519 x; + public Polynomial25519 z; + + public Montgomery(Polynomial25519 x, Polynomial25519 z) { + this.x = new Polynomial25519(x); + this.z = new Polynomial25519(z); + } + + public Montgomery(Montgomery m) { + this(m.x, m.z); + } + } + + public Polynomial25519() {} + + /** + * After, "this" is of reduced degree and of reduced coefficients. + * @param x An integer less than 2^26 in absolute value. + */ + public Polynomial25519(long x) { + c[0] = x; + } + + /** + * Copy constructor. + * After, "this" is of reduced degree. + * @param other A reduced degree polynomial. + */ + public Polynomial25519(Polynomial25519 other) { + for (int i = 0; i < 10; i++) { + c[i] = other.c[i]; + } + } + + /** + * Only used for testing. + */ + public Polynomial25519(long[] other) { + for (int i = 0; i < other.length && i < 19; i++) { + c[i] = other[i]; + } + } + + /** + * Constructs a polynomial from a 32-bytes representation. + */ + public Polynomial25519(byte[] bytes) { + if (bytes.length != 32) { + throw new IllegalArgumentException("bytes must have length 32"); + } + c[0] = coefficientFromBytes(bytes, 0, 0, 0x3ffffff); + c[1] = coefficientFromBytes(bytes, 3, 2, 0x1ffffff); + c[2] = coefficientFromBytes(bytes, 6, 3, 0x3ffffff); + c[3] = coefficientFromBytes(bytes, 9, 5, 0x1ffffff); + c[4] = coefficientFromBytes(bytes, 12, 6, 0x3ffffff); + c[5] = coefficientFromBytes(bytes, 16, 0, 0x1ffffff); + c[6] = coefficientFromBytes(bytes, 19, 1, 0x3ffffff); + c[7] = coefficientFromBytes(bytes, 22, 3, 0x1ffffff); + c[8] = coefficientFromBytes(bytes, 25, 4, 0x3ffffff); + c[9] = coefficientFromBytes(bytes, 28, 6, 0x1ffffff); + } + + /** + * Returns a single byte of the 32-bytes representation. + */ + private static long coefficientFromBytes(byte[] input, int start, int shift, int mask) { + return ((((input[start + 0] & 0xff) << 0) + | ((input[start + 1] & 0xff) << 8) + | ((input[start + 2] & 0xff) << 16) + | ((input[start + 3] & 0xff) << 24)) >> shift) & mask; + } + + /** + * Returns a 32-bytes representation of the element + */ + public byte[] toBytes() { + byte[] output = new byte[32]; + do { + for (int i = 0; i < 9; i++) { + if ((i & 1) == 1) { + while (c[i] < 0) { + c[i] += 0x2000000; + c[i + 1]--; + } + } else { + while (c[i] < 0) { + c[i] += 0x4000000; + c[i + 1]--; + } + } + } + while (c[9] < 0) { + c[9] += 0x2000000; + c[0] -= 19; + } + } while (c[0] < 0); + c[1] <<= 2; + c[2] <<= 3; + c[3] <<= 5; + c[4] <<= 6; + c[6] <<= 1; + c[7] <<= 3; + c[8] <<= 4; + c[9] <<= 6; + output[0] = 0; + output[16] = 0; + bytesFromCoefficients(output, 0, 0); + bytesFromCoefficients(output, 1, 3); + bytesFromCoefficients(output, 2, 6); + bytesFromCoefficients(output, 3, 9); + bytesFromCoefficients(output, 4, 12); + bytesFromCoefficients(output, 5, 16); + bytesFromCoefficients(output, 6, 19); + bytesFromCoefficients(output, 7, 22); + bytesFromCoefficients(output, 8, 25); + bytesFromCoefficients(output, 9, 28); + return output; + } + + private void bytesFromCoefficients(byte[] output, int idx, int start) { + output[start + 0] |= c[idx] & 0xff; + output[start + 1] = (byte) ((c[idx] >> 8) & 0xff); + output[start + 2] = (byte) ((c[idx] >> 16) & 0xff); + output[start + 3] = (byte) ((c[idx] >> 24) & 0xff); + } + + /** + * Clears the high-degree part of the polynomial, trusting the caller that that part is not + * used. + */ + public void clean() { + Arrays.fill(c, 10, 19, 0); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Polynomial25519 other = (Polynomial25519) obj; + for (int i = 0; i < 19; i++) { + if (other.c[i] != c[i]) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(c); + } + + public String toString() { + String s = ""; + for (int i = 0; i < 10; i++) { + s += c[i] + " "; + } + s += " "; + for (int i = 10; i < 19; i++) { + s += c[i] + " "; + } + return s; + } + + /** + * Computes the x coordinate of a point nQ, where Q is one of the preimages of q = q/1 in the + * elliptic curve. It is proven that any preimage Q gives the same result. + * @param n A number in the 32-bytes little endian format. + * @param q The x-coordinates of a point in the curve. + * @return The x coordinate of nQ, in the Montgomery form (first/second). Both polynomials are + * in the reduced degree form. + */ + public static Montgomery multiple(byte[] n, Polynomial25519 q) { + Montgomery nqpq[] = new Montgomery[] { + new Montgomery(new Polynomial25519(q), new Polynomial25519(1)), + new Montgomery(new Polynomial25519(0), new Polynomial25519(1)) + }; + Montgomery nq[] = new Montgomery[] { + new Montgomery(new Polynomial25519(1), new Polynomial25519(0)), + new Montgomery(new Polynomial25519(0), new Polynomial25519(1)) + }; + + int rollIdx = 0; + for (int i = 0; i < 32; i++) { + byte b = n[31 - i]; + for (int j = 0; j < 8; j++) { + int bit = -(b >> 7); + + swapConditional(nq[rollIdx % 2].x, nqpq[rollIdx % 2].x, bit); + swapConditional(nq[rollIdx % 2].z, nqpq[rollIdx % 2].z, bit); + + montgomery(nq[rollIdx % 2], nqpq[rollIdx % 2], q, + nq[(rollIdx + 1) % 2], nqpq[(rollIdx + 1) % 2]); + + swapConditional(nq[(rollIdx + 1) % 2].x, nqpq[(rollIdx + 1) % 2].x, bit); + swapConditional(nq[(rollIdx + 1) % 2].z, nqpq[(rollIdx + 1) % 2].z, bit); + + rollIdx++; + b <<= 1; + } + } + return nq[rollIdx % 2]; + } + + /** + * Adds other to the object. + * Before, "this" must be of reduced degree. + * After, "this" is of reduced degree. + * @param other A reduced degree polynomial. + */ + public void sum(Polynomial25519 other) { + for (int i = 0; i < 10; i++) { + c[i] += other.c[i]; + } + } + + /** + * Subtracts other from the object. + * Before, "this" must be of reduced degree. + * After, "this" is of reduced degree. + * @param other A reduced degree polynomial. + */ + public void diff(Polynomial25519 other) { + for (int i = 0; i < 10; i++) { + c[i] -= other.c[i]; + } + } + + /** + * Multiply the object by a scalar. + * Before, "this" must be of reduced degree and of reduced coefficients. + * After, "this" is of reduced degree and of reduced coefficients. + * @param scalar An integer less than 2^26. + */ + public void mult(int scalar) { + for (int i = 0; i < 10; i++) { + c[i] *= scalar; + } + reduceCoefficients(); + } + + /** + * Non-static version of {@code mult}. + */ + public void mult(Polynomial25519 other) { + c = Polynomial25519.innerMult(c, other.c); + reduceDegree(); + reduceCoefficients(); + } + + /** + * Returns a polynomial corresponding to the multiplication of the two polynomials in input. + * @param a A reduced degree, reduced coefficients polynomial. + * @param b A reduced degree, reduced coefficients polynomial. + * @return A reduced degree, reduced coefficients polynomial. + */ + public static Polynomial25519 mult(Polynomial25519 a, Polynomial25519 b) { + Polynomial25519 output = new Polynomial25519(); + output.c = Polynomial25519.innerMult(a.c, b.c); + output.reduceDegree(); + output.reduceCoefficients(); + return output; + } + + /** + * Returns a polynomial corresponding to the square of the input polynomial. + * @param a A reduced degree, reduced coefficients polynomial. + * @return A reduced degree, reduced coefficients polynomial. + */ + public static Polynomial25519 square(Polynomial25519 a) { + Polynomial25519 output = new Polynomial25519(); + output.c = Polynomial25519.innerSquare(a.c); + output.reduceDegree(); + output.reduceCoefficients(); + return output; + } + + /** + * Computes the square of this polynomial. + * Before, "this" must be of reduced degree, reduced coefficients. + * After, "this" is of reduced degree, reduced coefficients. + */ + public void square() { + c = Polynomial25519.innerSquare(c); + reduceDegree(); + reduceCoefficients(); + } + + /** + * Computes the reciprocal of the input by elevating to the (2^255 - 19) - 2 efficiently. + * "This" must be a reduced degree, reduced coefficient polynomial. + */ + public Polynomial25519 reciprocal() { + return innerReciprocal(this); + } + + /** + * Reduce the degree of the polynomial. + * Before, "this" must be of extended degree. + * After, "this" is of reduced degree. + */ + private void reduceDegree() { + for (int i = 8; i >= 0; i--) { + // Means adding 19 times the other. + c[i] += c[10 + i] << 4; + c[i] += c[10 + i] << 1; + c[i] += c[10 + i]; + } + // We do not need to zero the upper part, as following operations will assume they are not + // used. + } + + /** + * Reduce the coefficients of the polynomial. + * Before, "this" must be of reduced degree. + * After, "this" is of reduced degree, reduced coefficients. + */ + private void reduceCoefficients() { + do { + c[10] = 0; + for (int i = 0; i < 10; i += 2) { + long over = c[i] / 0x4000000; + c[i + 1] += over; + c[i] -= over * 0x4000000; + + over = c[i + 1] / 0x2000000; + c[i + 2] += over; + c[i + 1] -= over * 0x2000000; + } + c[0] += 19 * c[10]; + } while (c[10] != 0); + } + + /** + * Swaps a and b if iswap is 1, do nothing (but using the same time) if iswap is 0. Any other + * value is not accepted. + * @param a A reduced degree polynomial. + * @param b A reduced degree polynomial. + * @param iswap Whether should swap or not. + */ + private static void swapConditional(Polynomial25519 a, Polynomial25519 b, long iswap) { + int swap = (int) (-iswap); + for (int i = 0; i < 10; i++) { + int x = swap & (((int) a.c[i]) ^ ((int) b.c[i])); + a.c[i] = ((int) a.c[i]) ^ x; + b.c[i] = ((int) b.c[i]) ^ x; + } + } + + /** + * Computes the x coordinates of 2Q and Q+P. See appendix B (page 21) in [1]. + * @param q The x coordinate of a point Q on the curve, reduced degree form. Destroyed. + * @param p The x coordinate of a point P on the curve, reduced degree form. Destroyed. + * @param qmp The x coordinate of the point Q-P on the curve, reduced degree form. Preserved. + * @param qpq Output 2Q, in reduced degree, reduced coefficients form. + * @param qpp Output Q+P, in reduced degree, reduced coefficients form. + */ + private static void montgomery(Montgomery q, Montgomery p, Polynomial25519 qmp, + Montgomery qpq, Montgomery qpp) { + Montgomery qprime = new Montgomery(q); + qprime.x.sum(q.z); // q'.x = q.x + q.z + qprime.z.diff(q.x); // q'.z = q.z - q.x + + Montgomery pprime = new Montgomery(p); + pprime.x.sum(p.z); // p'.x = p.x + p.z + pprime.z.diff(p.x); // p'.z = p.z - p.x + + pprime.x.mult(qprime.z); // p'.x *= q'.z + pprime.z.mult(qprime.x); // p'.z *= q'.x + + qprime.x.square(); // q'.x **= 2 + qprime.z.square(); // q'.z **= 2 + + qpp.x = new Polynomial25519(pprime.x); + qpp.z = new Polynomial25519(pprime.z); + qpp.x.sum(pprime.z); // (q+p).x = p'.x + p'.z + qpp.z.diff(pprime.x); // (q+p).z = p'.z - p'.x + + qpp.x.square(); // (q+p).x **= 2 + qpp.z.square(); // (q+p).z **= 2 + qpp.z.mult(qmp); // (q+p).z *= (q-p) + + qpq.x = new Polynomial25519(qprime.x); + qpq.z = new Polynomial25519(qprime.x); + qpq.x.mult(qprime.z); // (2q).x = q'.x * q'.z + qpq.z.diff(qprime.z); // (2q).z = q'.x - q'.z + + Polynomial25519 t = new Polynomial25519(qpq.z); + qpq.z.mult(121665); // (2q).z *= (A - 2) / 4 + qpq.z.sum(qprime.x); // (2q).z += q'.x + qpq.z.mult(t); // (2q).z += t + } + + + /** + * Returns a polynomial corresponding to the multiplication of the two polynomials in input. + * @param a A reduced degree, reduced coefficients polynomial. + * @param b A reduced degree, reduced coefficients polynomial. + * @return The product polynomial. + */ + private static long[] innerMult(long[] a, long[] b) { + long[] output = new long[19]; + output[0] = + b[0] * a[0]; + output[1] = + b[0] * a[1] + + b[1] * a[0]; + output[2] = + b[1] * a[1] * 2 + + b[0] * a[2] + + b[2] * a[0]; + output[3] = + b[1] * a[2] + + b[2] * a[1] + + b[0] * a[3] + + b[3] * a[0]; + output[4] = + b[2] * a[2] + + (b[1] * a[3] + + b[3] * a[1]) * 2 + + b[0] * a[4] + + b[4] * a[0]; + output[5] = + b[2] * a[3] + + b[3] * a[2] + + b[1] * a[4] + + b[4] * a[1] + + b[0] * a[5] + + b[5] * a[0]; + output[6] = + (b[3] * a[3] + + b[1] * a[5] + + b[5] * a[1]) * 2 + + b[2] * a[4] + + b[4] * a[2] + + b[0] * a[6] + + b[6] * a[0]; + output[7] = + b[3] * a[4] + + b[4] * a[3] + + b[2] * a[5] + + b[5] * a[2] + + b[1] * a[6] + + b[6] * a[1] + + b[0] * a[7] + + b[7] * a[0]; + output[8] = + b[4] * a[4] + + (b[3] * a[5] + + b[5] * a[3] + + b[1] * a[7] + + b[7] * a[1]) * 2 + + b[2] * a[6] + + b[6] * a[2] + + b[0] * a[8] + + b[8] * a[0]; + output[9] = + b[4] * a[5] + + b[5] * a[4] + + b[3] * a[6] + + b[6] * a[3] + + b[2] * a[7] + + b[7] * a[2] + + b[1] * a[8] + + b[8] * a[1] + + b[0] * a[9] + + b[9] * a[0]; + output[10] = + (b[5] * a[5] + + b[3] * a[7] + + b[7] * a[3] + + b[1] * a[9] + + b[9] * a[1]) * 2 + + b[4] * a[6] + + b[6] * a[4] + + b[2] * a[8] + + b[8] * a[2]; + output[11] = + b[5] * a[6] + + b[6] * a[5] + + b[4] * a[7] + + b[7] * a[4] + + b[3] * a[8] + + b[8] * a[3] + + b[2] * a[9] + + b[9] * a[2]; + output[12] = + b[6] * a[6] + + (b[5] * a[7] + + b[7] * a[5] + + b[3] * a[9] + + b[9] * a[3]) * 2 + + b[4] * a[8] + + b[8] * a[4]; + output[13] = + b[6] * a[7] + + b[7] * a[6] + + b[5] * a[8] + + b[8] * a[5] + + b[4] * a[9] + + b[9] * a[4]; + output[14] = + (b[7] * a[7] + + b[5] * a[9] + + b[9] * a[5]) * 2 + + b[6] * a[8] + + b[8] * a[6]; + output[15] = + b[7] * a[8] + + b[8] * a[7] + + b[6] * a[9] + + b[9] * a[6]; + output[16] = + b[8] * a[8] + + (b[7] * a[9] + + b[9] * a[7]) * 2; + output[17] = + b[8] * a[9] + + b[9] * a[8]; + output[18] = + b[9] * a[9] * 2; + return output; + } + + /** + * Returns a polynomial corresponding to the square of the input polynomial. + * @param a A reduced degree, reduced coefficients polynomial. + * @return The squared polynomial. + */ + private static long[] innerSquare(long[] a) { + long[] output = new long[19]; + output[0] = + a[0] * a[0]; + output[1] = + a[0] * a[1] * 2; + output[2] = + (a[1] * a[1] + + a[0] * a[2]) * 2; + output[3] = + (a[1] * a[2] + + a[0] * a[3]) * 2; + output[4] = + a[2] * a[2] + + a[1] * a[3] * 4 + + a[0] * a[4] * 2; + output[5] = + (a[2] * a[3] + + a[1] * a[4] + + a[0] * a[5]) * 2; + output[6] = + (a[3] * a[3] + + a[2] * a[4] + + a[0] * a[6] + + a[1] * a[5] * 2) * 2; + output[7] = + (a[3] * a[4] + + a[2] * a[5] + + a[1] * a[6] + + a[0] * a[7]) * 2; + output[8] = + a[4] * a[4] + + (a[2] * a[6] + + a[0] * a[8] + + (a[1] * a[7] + + a[3] * a[5]) * 2) * 2; + output[9] = + (a[4] * a[5] + + a[3] * a[6] + + a[2] * a[7] + + a[1] * a[8] + + a[0] * a[9]) * 2; + output[10] = (a[5] * a[5] + + a[4] * a[6] + + a[2] * a[8] + + (a[3] * a[7] + + a[1] * a[9]) * 2) * 2; + output[11] = + (a[5] * a[6] + + a[4] * a[7] + + a[3] * a[8] + + a[2] * a[9]) * 2; + output[12] = + a[6] * a[6] + + (a[4] * a[8] + + (a[5] * a[7] + + a[3] * a[9]) * 2) * 2; + output[13] = + (a[6] * a[7] + + a[5] * a[8] + + a[4] * a[9]) * 2; + output[14] = + (a[7] * a[7] + + a[6] * a[8] + + a[5] * a[9] * 2) * 2; + output[15] = + (a[7] * a[8] + + a[6] * a[9]) * 2; + output[16] = + a[8] * a[8] + + a[7] * a[9] * 4; + output[17] = + a[8] * a[9] * 2; + output[18] = + a[9] * a[9] * 2; + return output; + } + + /** + * Computes the reciprocal of the input by elevating to the (2^255 - 19) - 2 efficiently. + * "This" must be a reduced degree, reduced coefficient polynomial. + */ + private static Polynomial25519 innerReciprocal(Polynomial25519 z) { + // In the comment we wrote the exponent of the input. + // TODO: can avoid t1? + /* 2 */ Polynomial25519 z2 = Polynomial25519.square(z); + /* 4 */ Polynomial25519 t1 = Polynomial25519.square(z2); + /* 8 */ Polynomial25519 t0 = Polynomial25519.square(t1); + /* 9 */ Polynomial25519 z9 = Polynomial25519.mult(t0, z); + /* 11 */ Polynomial25519 z11 = Polynomial25519.mult(z9, z2); + /* 22 */ t0 = Polynomial25519.square(z11); + /* 31 = 2^5 - 2^0 */ Polynomial25519 z2FiveZero = Polynomial25519.mult(t0, z9); + + /* 2^6 - 2^1 */ t0 = Polynomial25519.square(z2FiveZero); + /* 2^7 - 2^2 */ t1 = Polynomial25519.square(t0); + /* 2^8 - 2^3 */ t0 = Polynomial25519.square(t1); + /* 2^9 - 2^4 */ t1 = Polynomial25519.square(t0); + /* 2^10 - 2^5 */ t0 = Polynomial25519.square(t1); + /* 2^10 - 2^0 */ Polynomial25519 z2TenZero = Polynomial25519.mult(t0, z2FiveZero); + + /* 2^11 - 2^1 */ t0 = Polynomial25519.square(z2TenZero); + /* 2^12 - 2^2 */ t1 = Polynomial25519.square(t0); + /* 2^20 - 2^10 */ + for (int i = 2; i < 10; i += 2) { + t0 = Polynomial25519.square(t1); + t1 = Polynomial25519.square(t0); + } + /* 2^20 - 2^0 */ Polynomial25519 z2TwentyZero = Polynomial25519.mult(t1, z2TenZero); + + /* 2^21 - 2^1 */ t0 = Polynomial25519.square(z2TwentyZero); + /* 2^22 - 2^2 */ t1 = Polynomial25519.square(t0); + /* 2^40 - 2^20 */ + for (int i = 2; i < 20; i += 2) { + t0 = Polynomial25519.square(t1); + t1 = Polynomial25519.square(t0); + } + /* 2^40 - 2^0 */ t0 = Polynomial25519.mult(t1, z2TwentyZero); + + /* 2^41 - 2^1 */ t1 = Polynomial25519.square(t0); + /* 2^42 - 2^2 */ t0 = Polynomial25519.square(t1); + /* 2^50 - 2^10 */ + for (int i = 2; i < 10; i += 2) { + t1 = Polynomial25519.square(t0); + t0 = Polynomial25519.square(t1); + } + /* 2^50 - 2^0 */ Polynomial25519 z2FiftyZero = Polynomial25519.mult(t0, z2TenZero); + + /* 2^51 - 2^1 */ t0 = Polynomial25519.square(z2FiftyZero); + /* 2^52 - 2^2 */ t1 = Polynomial25519.square(t0); + /* 2^100 - 2^50 */ + for (int i = 2; i < 50; i += 2) { + t0 = Polynomial25519.square(t1); + t1 = Polynomial25519.square(t0); + } + /* 2^100 - 2^0 */ Polynomial25519 z2HundredZero = Polynomial25519.mult(t1, z2FiftyZero); + + /* 2^101 - 2^1 */ t1 = Polynomial25519.square(z2HundredZero); + /* 2^102 - 2^2 */ t0 = Polynomial25519.square(t1); + /* 2^200 - 2^100 */ + for (int i = 2; i < 100; i += 2) { + t1 = Polynomial25519.square(t0); + t0 = Polynomial25519.square(t1); + } + /* 2^200 - 2^0 */ t1 = Polynomial25519.mult(t0, z2HundredZero); + + /* 2^201 - 2^1 */ t0 = Polynomial25519.square(t1); + /* 2^202 - 2^2 */ t1 = Polynomial25519.square(t0); + /* 2^250 - 2^50 */ + for (int i = 2; i < 50; i += 2) { + t0 = Polynomial25519.square(t1); + t1 = Polynomial25519.square(t0); + } + /* 2^250 - 2^0 */ t0 = Polynomial25519.mult(t1, z2FiftyZero); + + /* 2^251 - 2^1 */ t1 = Polynomial25519.square(t0); + /* 2^252 - 2^2 */ t0 = Polynomial25519.square(t1); + /* 2^253 - 2^3 */ t1 = Polynomial25519.square(t0); + /* 2^254 - 2^4 */ t0 = Polynomial25519.square(t1); + /* 2^255 - 2^5 */ t1 = Polynomial25519.square(t0); + /* 2^255 - 21 */ return Polynomial25519.mult(t1, z11); + } + + } +} diff --git a/app/src/main/java/eidr/EddystoneEidrGenerator.java b/app/src/main/java/eidr/EddystoneEidrGenerator.java new file mode 100644 index 0000000..ca9a364 --- /dev/null +++ b/app/src/main/java/eidr/EddystoneEidrGenerator.java @@ -0,0 +1,149 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package eidr; + +import android.util.Log; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + + +/** + * A sample implementation of Eddystone EIDR computation. + */ +public class EddystoneEidrGenerator { + private static final String TAG = EddystoneEidrGenerator.class.getSimpleName(); + + // The server's public ECDH Curve25519 key. Must be 32 bytes. + private byte[] serviceEcdhPublicKey; + + // The beacon's private key. Randomly generated 32-byte data. + private byte[] beaconPrivateKey; + + // The beacon's public key computed from the private key over ECDH Curve25519. + private byte[] beaconPublicKey; + + // In some test scenarios we may want to broadcast EIDs from a known Identity Key. + private byte[] beaconIdentityKey; + + /** + * Constructs an EddystoneEidrGenerator instance with a real beacon private key and a real + * service public key. + * + * @param serviceEcdhPublicKey 32-byte public key of remote server + * @param beaconEcdhPrivateKey 32-byte private key of beacon + */ + public EddystoneEidrGenerator(byte[] serviceEcdhPublicKey, byte[] beaconEcdhPrivateKey) { + checkArgument(serviceEcdhPublicKey != null && serviceEcdhPublicKey.length == 32); + checkArgument(beaconEcdhPrivateKey != null && beaconEcdhPrivateKey.length == 32); + this.serviceEcdhPublicKey = serviceEcdhPublicKey; + this.beaconPrivateKey = beaconEcdhPrivateKey; + beaconPublicKey = generateBeaconPublicKey(); + } + + /** + * Getter for the beacon Roshan + */ + public byte [] getBeaconPublicKey(){ + if(beaconPublicKey != null) + return beaconPublicKey; + return null; + } + + /** + * Sets the beacon's identity key. When set, this generator will skip the earlier steps of the + * crypto process that create an identity key (via ECDH, etc). + * + * Such use would look like: + * + * byte[] identityKey = getMyBeaconsIdentityKeyFromSomewhere(); + * int rotationExponent = getMyBeaconsRotationExponentFromThatSamePlace(); + * long beaconClockOffset = getMyBeaconsClockOffsetFromThatSamePlace(); + * long beaconNowMs = System.currentTimeMillis() - beaconClockOffset; + * EddystoneEidrGenerator eidGen = new EddystoneEidrGenerator(); + * eidGen.setIdentityKey(identityKey); + * byte[] ephemeralId = eidGen(rotationExponent, (int)(beaconNowMs / 1000)); + * + * + * @param beaconIdentityKey + */ + public void setIdentityKey(byte[] beaconIdentityKey) { + this.beaconIdentityKey = beaconIdentityKey; + // When using a given identity key, the beacon's public and private keys are unused. Nullify + // them to make sure. + beaconPublicKey = null; + beaconPrivateKey = null; + } + + /** + * Returns the beacon's public key. + */ + public byte[] generateBeaconPublicKey() { + return Curve25519.scalarMultBase(beaconPrivateKey); + } + + byte[] getSharedSecret() { + return Curve25519.scalarMult(beaconPrivateKey, serviceEcdhPublicKey); + } + + public byte[] getIdentityKey() { + if (beaconIdentityKey != null) { + return beaconIdentityKey; + } + Mac hkdfSha256Mac; + try { + hkdfSha256Mac = Mac.getInstance("HmacSHA256"); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "Error constructing SHA256 HMAC instance", e); + return null; // XXX fix callers to cope with null + } + + byte[] publicKeys = new byte[serviceEcdhPublicKey.length + beaconPublicKey.length]; + System.arraycopy(serviceEcdhPublicKey, 0, publicKeys, 0, serviceEcdhPublicKey.length); + System.arraycopy(beaconPublicKey, 0, publicKeys, serviceEcdhPublicKey.length, beaconPublicKey.length); + + try { + hkdfSha256Mac.init(new SecretKeySpec(publicKeys, "AES")); + } catch (InvalidKeyException e) { + Log.e(TAG, "Error initializing SHA256 HMAC instance", e); + return null; + } + + byte[] sharedSecret = hkdfSha256Mac.doFinal(getSharedSecret()); + if (sharedSecret == null) { + Log.e(TAG, "Shared secret is zero. Possibly indicates a weak public key!"); + return null; + } + + try { + hkdfSha256Mac.init(new SecretKeySpec(sharedSecret, "AES")); + } catch (InvalidKeyException e) { + Log.e(TAG, "Error reinitializing SHA256 HMAC instance", e); + return null; + } + + byte[] salt = { 0x01 }; + return Arrays.copyOfRange(hkdfSha256Mac.doFinal(salt), 0, 16); + } + + private void checkArgument(boolean b) { + if (!b) { + throw new IllegalArgumentException(); + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/AuthorizedServiceTask.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/AuthorizedServiceTask.java new file mode 100644 index 0000000..1a1ed34 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/AuthorizedServiceTask.java @@ -0,0 +1,113 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package no.nordicsemi.android.nrfbeacon.nearby; + +import android.accounts.Account; +import android.app.Activity; +import android.app.Dialog; +import android.content.Intent; +import android.os.AsyncTask; +import android.util.Log; + +import com.google.android.gms.auth.GoogleAuthException; +import com.google.android.gms.auth.GoogleAuthUtil; +import com.google.android.gms.auth.GooglePlayServicesAvailabilityException; +import com.google.android.gms.auth.UserRecoverableAuthException; +import com.google.android.gms.common.GooglePlayServicesUtil; + +import java.io.IOException; + +/** + * NOP async task that allows us to check if a new user has authorized the app + * to access their account. + */ +public class AuthorizedServiceTask extends AsyncTask { + private static final String TAG = AuthorizedServiceTask.class.getSimpleName(); + //static final String authScope = "oauth2:https://www.googleapis.com/auth/userlocation.beacon.registry"; + private static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_PRX_API = 1003; + private static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_URL_SHORTNER = 1004; + private static final String AUTH_PROXIMITY_API = "oauth2:https://www.googleapis.com/auth/userlocation.beacon.registry"; + private static final String AUTH_SCOPE_URL_SHORTENER = "oauth2:https://www.googleapis.com/auth/urlshortener"; + + private final Activity activity; + private final Account account; + private String authScope; + + public AuthorizedServiceTask(Activity activity, Account accountName, final String authScope) { + this.activity = activity; + this.account = accountName; + this.authScope = authScope; + } + + @Override + protected String doInBackground(Void... params) { + Log.i(TAG, "checking authorization for " + account.name); + try { + final String token = GoogleAuthUtil.getToken(activity, account, authScope); + if(token != null){ + + } + } catch (UserRecoverableAuthException e) { + // GooglePlayServices.apk is either old, disabled, or not present + // so we need to show the user some UI in the activity to recover. + handleAuthException(activity, e); + } catch (GoogleAuthException e) { + // Some other type of unrecoverable exception has occurred. + // Report and log the error as appropriate for your app. + Log.w(TAG, "GoogleAuthException: " + e); + } catch (IOException e) { + // The fetchToken() method handles Google-specific exceptions, + // so this indicates something went wrong at a higher level. + // TIP: Check for network connectivity before starting the AsyncTask. + } + return null; + } + + private void handleAuthException(final Activity activity, final Exception e) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (e instanceof GooglePlayServicesAvailabilityException) { + // The Google Play services APK is old, disabled, or not present. + // Show a dialog created by Google Play services that allows + // the user to update the APK + int statusCode = ((GooglePlayServicesAvailabilityException) e).getConnectionStatusCode(); + Dialog dialog; + if(authScope.equals(AUTH_PROXIMITY_API)) { + dialog = GooglePlayServicesUtil.getErrorDialog( + statusCode, activity, REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_PRX_API); + } else { + dialog = GooglePlayServicesUtil.getErrorDialog( + statusCode, activity, REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_URL_SHORTNER); + } + dialog.show(); + } else if (e instanceof UserRecoverableAuthException) { + // Unable to authenticate, such as when the user has not yet granted + // the app access to the account, but the user can fix this. + // Forward the user to an activity in Google Play services. + Intent intent = ((UserRecoverableAuthException) e).getIntent(); + if(authScope.equals(AUTH_PROXIMITY_API)) { + activity.startActivityForResult( + intent, REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_PRX_API); + } else { + activity.startActivityForResult( + intent, REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_URL_SHORTNER); + } + } + } + }); + } + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/EddystoneBeaconsAdapter.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/EddystoneBeaconsAdapter.java new file mode 100644 index 0000000..ea8a8ab --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/EddystoneBeaconsAdapter.java @@ -0,0 +1,107 @@ +package no.nordicsemi.android.nrfbeacon.nearby; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Paint; +import android.net.Uri; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.UnderlineSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import com.google.android.gms.nearby.messages.Message; + +import java.nio.charset.Charset; +import java.util.ArrayList; + +/** + * Created by rora on 22.10.2015. + */ +public class EddystoneBeaconsAdapter extends BaseAdapter { + + private ArrayList nearbyDeviceMessageList; + private LayoutInflater mInflator; + private Context context; + + public EddystoneBeaconsAdapter(Context context, ArrayList nearbyDevicesMessage) { + super(); + nearbyDeviceMessageList = nearbyDevicesMessage; + this.context = context; + mInflator = (LayoutInflater) this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public int getCount() { + return nearbyDeviceMessageList.size(); + } + + @Override + public Object getItem(int position) { + return nearbyDeviceMessageList.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + public void clear() { + nearbyDeviceMessageList.clear(); + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + + ViewHolder viewHolder; + if (convertView == null) { + convertView = mInflator.inflate(R.layout.listitem_device, null); + viewHolder = new ViewHolder(); + viewHolder.tvAttachment = (TextView) convertView.findViewById(R.id.attachment_message); + viewHolder.tvNamespace = (TextView) convertView.findViewById(R.id.namespace_message); + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + + Message message = nearbyDeviceMessageList.get(position); + String namespace = message.getNamespace(); + final String attachment = new String(message.getContent(), Charset.forName("UTF-8")); + SpannableString attachmentContent = null; + if(attachment.startsWith("http")) { + attachmentContent = new SpannableString(attachment); + attachmentContent.setSpan(new UnderlineSpan(), 0, attachment.length(), 0); + viewHolder.tvAttachment.setText(attachmentContent); + } else { + viewHolder.tvAttachment.setText(attachment); + } + if (namespace != null && namespace.length() > 0) + viewHolder.tvNamespace.setText(namespace); + //viewHolder.tvAttachment.setText(attachment); + + if(attachmentContent != null && attachmentContent.toString().startsWith("http")) { + //viewHolder.tvAttachment.setPaintFlags(viewHolder.tvAttachment.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + viewHolder.tvAttachment.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(attachment)); + context.startActivity(browserIntent); + } + }); + } else { + viewHolder.tvAttachment.setOnClickListener(null); + } + + return convertView; + } + + private static class ViewHolder { + TextView tvNamespace; + TextView tvAttachment; + } +} + diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/LaunchActivity.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/LaunchActivity.java new file mode 100644 index 0000000..7fa9680 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/LaunchActivity.java @@ -0,0 +1,48 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ + +package no.nordicsemi.android.nrfbeacon.nearby; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +public class LaunchActivity extends AppCompatActivity { + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // If this activity is the root activity of the task, the app is not running + if (isTaskRoot()) { + // Start the app before finishing + final Intent startAppIntent = new Intent(getApplicationContext(), SplashscreenActivity.class); + startAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (getIntent() != null && getIntent().getExtras() != null) + startAppIntent.putExtras(getIntent().getExtras()); + startActivity(startAppIntent); + } + + // Now finish, which will drop the user in to the activity that was at the top + // of the task stack + finish(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/MainActivity.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/MainActivity.java new file mode 100644 index 0000000..ddd149a --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/MainActivity.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.MenuItem; +import android.widget.Toast; + +import no.nordicsemi.android.nrfbeacon.nearby.beacon.BeaconsFragment; +import no.nordicsemi.android.nrfbeacon.nearby.update.UpdateFragment; + +public class MainActivity extends AppCompatActivity { + + public static final String OPENED_FROM_LAUNCHER = "no.nordicsemi.android.nrfbeacon.nearby.extra.opened_from_launcher"; + public static final String TAG = "BEACON"; + private static final int REQUEST_RESOLVE_ERROR = 261; //random + private static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_PRX_API = 1003; + private static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_URL_SHORTNER = 1004; + + private BeaconsFragment mBeaconsFragment; + private UpdateFragment mUpdateFragment; + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode){ + case REQUEST_RESOLVE_ERROR: + if(resultCode == Activity.RESULT_OK){ + mBeaconsFragment.updateNearbyPermissionStatus(true); + } + else Toast.makeText(this, getString(R.string.rationale_permission_denied), Toast.LENGTH_SHORT).show(); + break; + case REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_PRX_API: + if(resultCode == Activity.RESULT_OK){ + Toast.makeText(this, getString(R.string.retry_beacon_registration), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, getString(R.string.rationale_permission_denied) + ". Unable to register beacon", Toast.LENGTH_SHORT).show(); + } + break; + case REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_URL_SHORTNER: + if(resultCode == Activity.RESULT_OK){ + Toast.makeText(this, getString(R.string.retry_url_shortner), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, getString(R.string.rationale_permission_denied) + ". Unable to shorten URL", Toast.LENGTH_SHORT).show(); + } + break; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Ensure that Bluetooth exists + if (!ensureBleExists()) + finish(); + + // Setup the custom toolbar + final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + // Prepare the sliding tab layout and the view pager + final TabLayout tabLayout = (TabLayout) findViewById(R.id.sliding_tabs); + final ViewPager pager = (ViewPager) findViewById(R.id.view_pager); + //pager.setOffscreenPageLimit(2); + pager.setAdapter(new FragmentAdapter(getSupportFragmentManager())); + tabLayout.setupWithViewPager(pager); + pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + + @Override + public void onPageSelected(final int position) { + Log.v(TAG, "position: " + position); + switch (position){ + case 1: + mUpdateFragment.ensurePermissionGranted(new String[]{Manifest.permission.GET_ACCOUNTS}); + break; + } + } + + @Override + public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) { + // empty + } + + @Override + public void onPageScrollStateChanged(int state) { + } + }); + + + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + } + return false; + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + // we are in main fragment, show 'home up' if entered from Launcher (splash screen activity) + final boolean openedFromLauncher = getIntent().getBooleanExtra(MainActivity.OPENED_FROM_LAUNCHER, false); + getSupportActionBar().setDisplayHomeAsUpEnabled(!openedFromLauncher); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onStop() { + super.onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + /** + * Checks whether the device supports Bluetooth Low Energy communication + * + * @return true if BLE is supported, false otherwise + */ + private boolean ensureBleExists() { + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { + Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show(); + return false; + } + return true; + } + + public void setBeaconsFragment(BeaconsFragment beaconsFragment) { + this.mBeaconsFragment = beaconsFragment; + } + + public void setUPdateFragment(UpdateFragment updateFragment) { + this.mUpdateFragment = updateFragment; + } + + private class FragmentAdapter extends FragmentPagerAdapter { + + public FragmentAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case 0: + return new BeaconsFragment(); + default: + case 1: + return new UpdateFragment(); + } + } + + @Override + public int getCount() { + return 2; + } + + @Override + public CharSequence getPageTitle(int position) { + return getResources().getStringArray(R.array.tab_title)[position]; + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/NearbyBackgroundService.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/NearbyBackgroundService.java new file mode 100644 index 0000000..1098f85 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/NearbyBackgroundService.java @@ -0,0 +1,102 @@ +package no.nordicsemi.android.nrfbeacon.nearby; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import com.google.android.gms.nearby.Nearby; +import com.google.android.gms.nearby.messages.Message; +import com.google.android.gms.nearby.messages.MessageListener; + +import java.nio.charset.Charset; +import java.util.ArrayList; + +/** + * Created by rora on 28.01.2016. + */ +public class NearbyBackgroundService extends IntentService { + + private static final String DISPLAY_NOTIFICATION = "no.nordicsemi.android.nrfbeacon.nearby.DISPLAY_NOTIFICATION"; + private static final String REMOVE_NOTIFICATION = "no.nordicsemi.android.nrfbeacon.nearby.REMOVE_NOTIFICATION"; + private static final String NEARBY_MESSAGE = "no.nordicsemi.android.nrfbeacon.nearby.NEARBY_MESSAGE"; + public static final String NEARBY_DEVICE_DATA = "NEARBY_DEVICE_DATA"; + private static final String TAG = "BEACON"; + + private static final int OPEN_ACTIVITY_REQ = 195; // random + private static final int NOTIFICATION_ID = 1; + + private NotificationManager mNotificationManager; + private Intent mParentIntent; + private PendingIntent mPendingIntent; + private ArrayList mNearbyDevicesMessageList; + + public NearbyBackgroundService(String name) { + super(name); + } + + public NearbyBackgroundService() { + super("NearbyBackgroundService"); + } + + @Override + public void onCreate() { + super.onCreate(); + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mNearbyDevicesMessageList = new ArrayList<>(); + } + + + @Override + public void onDestroy() { + super.onDestroy(); + Log.v(TAG, "Destroyed"); + } + + @Override + protected void onHandleIntent(final Intent intent) { + + Nearby.Messages.handleIntent(intent, new MessageListener() { + @Override + public void onFound(Message message) { + String nearbyMessage = new String(message.getContent(), Charset.forName("UTF-8")); + Log.i(TAG, "Found message via PendingIntent: " + nearbyMessage); + sendMessage(DISPLAY_NOTIFICATION, message); + } + + @Override + public void onLost(Message message) { + String nearbyMessage = new String(message.getContent(), Charset.forName("UTF-8")); + Log.i(TAG, "Lost message via PendingIntent: " + nearbyMessage); + sendMessage(REMOVE_NOTIFICATION, message); + } + }); + } + + private void sendMessage(String broadcast, Message message) { + Intent intent; + Bundle bundle; + switch (broadcast){ + case DISPLAY_NOTIFICATION: + intent = new Intent(broadcast); + bundle = new Bundle(); + bundle.putParcelable(NEARBY_MESSAGE, message); + intent.putExtras(bundle); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + break; + case REMOVE_NOTIFICATION: + intent = new Intent(broadcast); + bundle = new Bundle(); + bundle.putParcelable(NEARBY_MESSAGE, message); + intent.putExtras(bundle); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + break; + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/SplashscreenActivity.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/SplashscreenActivity.java new file mode 100644 index 0000000..1454faf --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/SplashscreenActivity.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; + +public class SplashscreenActivity extends Activity { + /** Splash screen duration time in milliseconds */ + private static final int DELAY = 1000; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_splashscreen); + + // Jump to MainActivity after DELAY milliseconds + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + final Intent intent = new Intent(SplashscreenActivity.this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.putExtra(MainActivity.OPENED_FROM_LAUNCHER, true); + startActivity(intent); + finish(); + } + }, DELAY); + } + + @Override + public void onBackPressed() { + // do nothing. Protect from exiting the application when splash screen is shown + } + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/UpdateService.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/UpdateService.java new file mode 100644 index 0000000..a4c486b --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/UpdateService.java @@ -0,0 +1,1145 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.UUID; + +import android.app.Service; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.Intent; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.ParcelUuid; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import android.util.Pair; + +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +public class UpdateService extends Service { + private static final String TAG = "UpdateService"; + + public final static String ACTION_STATE_CHANGED = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_STATE_CHANGED"; + public final static String ACTION_GATT_ERROR = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_GATT_ERROR"; + public final static String ACTION_DONE = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_DONE"; + public final static String ACTION_UUID_READY = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_UUID_READY"; + public final static String ACTION_MAJOR_MINOR_READY = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_MAJOR_MINOR_READY"; + public final static String ACTION_RSSI_READY = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_RSSI_READY"; + public final static String ACTION_MANUFACTURER_ID_READY = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_MANUFACTURER_ID_READY"; + public final static String ACTION_ADV_INTERVAL_READY = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_ADV_INTERVAL_READY"; + public final static String ACTION_LED_STATUS_READY = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_LED_STATUS_READY"; + + public final static String ACTION_BROADCAST_CAPABILITIES = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_BROADCAST_CAPABILITIES"; + public final static String ACTION_ACTIVE_SLOT = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_ACTIVE_SLOT"; + public final static String ACTION_ADVERTISING_INTERVAL = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_ADVERTISING_INTERVAL"; + public final static String ACTION_RADIO_TX_POWER = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_RADIO_TX_POWER"; + public final static String ACTION_ADVANCED_ADVERTISED_TX_POWER = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_ADVANCED_ADVERTISED_TX_POWER"; + public final static String ACTION_LOCK_STATE = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_LOCK_STATE"; + public final static String ACTION_UNLOCK = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_UNLOCK"; + public final static String ACTION_ECDH_KEY = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_ECDH_KEY"; + public final static String ACTION_EID_IDENTITY_KEY = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_EID_IDENTITY_KEY"; + public final static String ACTION_READ_WRITE_ADV_SLOT = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_READ_WRITE_ADV_SLOT"; + public final static String ACTION_ADVANCED_FACTORY_RESET = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_ADVANCED_FACTORY_RESET"; + public final static String ACTION_ADVANCED_REMAIN_CONNECTABLE = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_ADVANCED_REMAIN_CONNECTABLE"; + public final static String ACTION_UNLOCK_BEACON = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_UNLOCK_BEACON"; + public static final String ACTION_BROADCAST_ALL_SLOT_INFO = "no.nordicsemi.android.nrfbeacon.nearby.ACTION_BROADCAST_ALL_SLOT_INFO"; + + + public final static String EXTRA_DATA = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_DATA"; + public final static String EXTRA_FRAME_TYPE = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_FRAME_TYPE"; + public final static String EXTRA_NAMESPACE_ID = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_NAMESPACE_ID"; + public final static String EXTRA_INSTANCE_ID = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_INSTANCE_ID"; + public final static String EXTRA_URL = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_URL"; + public final static String EXTRA_CLOCK_VALUE = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_CLOCK_VALUE"; + public final static String EXTRA_TIMER_EXPONENT = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_TIMER_EXPONENT"; + public final static String EXTRA_EID = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_EID"; + public final static String EXTRA_VOLTAGE = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_VOLTAGE"; + public final static String EXTRA_BEACON_TEMPERATURE = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_BEACON_TEMPERATURE"; + public final static String EXTRA_PDU_COUNT = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_PDU_COUNT"; + public final static String EXTRA_TIME_SINCE_BOOT = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_TIME_SINCE_BOOT"; + public final static String EXTRA_ETLM = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_ETLM"; + public final static String EXTRA_SALT = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_SALT"; + public final static String EXTRA_MESSAGE_INTEGRITY_CHECK = "no.nordicsemi.android.nrfbeacon.nearby.EXTRA_MESSAGE_INTEGRITY_CHECK"; + + private static final int EMPTY_SLOT = -1; + private static final int TYPE_UID = 0x00; + private static final int TYPE_URL = 0x10; + private static final int TYPE_TLM = 0x20; + private static final int TYPE_EID = 0x30; + + public static final int LOCKED = 0x00; + public static final int UNLOCKED = 0x01; + public static final int UNLOCKED_AUTOMATIC_RELOCK_DISABLED = 0x02; + + public final static int ERROR_UNSUPPORTED_DEVICE = -1; + + private int mConnectionState; + public final static int STATE_DISCONNECTED = 0; + public final static int STATE_CONNECTING = 1; + public final static int STATE_DISCOVERING_SERVICES = 2; + public final static int STATE_CONNECTED = 3; + public final static int STATE_DISCONNECTING = 4; + + public final static int SERVICE_UUID = 1; + public final static int SERVICE_MAJOR_MINOR = 2; + public final static int SERVICE_CALIBRATION = 3; + + public static final UUID EDDYSTONE_GATT_CONFIG_SERVICE_UUID = new UUID(0xA3C875008ED34BDFL, 0x8A39A01BEBEDE295L); + private static final UUID EDDYSTONE_BROADCAST_CAPABILITIES_UUID = new UUID(0xA3C875018ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_ACTIVE_SLOT_UUID = new UUID(0xA3C875028ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_ADVERTISING_INTERVAL_UUID = new UUID(0xA3C875038ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_RADIO_TX_POWER_UUID = new UUID(0xA3C875048ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_ADVANCED_ADVERTISED_TX_POWER_UUID = new UUID(0xA3C875058ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_LOCK_STATE_UUID = new UUID(0xA3C875068ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_UNLOCK_UUID = new UUID(0xA3C875078ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_ECDH_KEY_UUID = new UUID(0xA3C875088ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_EID_IDENTITY_KEY_UUID = new UUID(0xA3C875098ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_READ_WRITE_ADV_SLOT_UUID = new UUID(0xA3C8750A8ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_ADVANCED_FACTORY_RESET_UUID = new UUID(0xA3C8750B8ed34bdfL, 0x8a39a01bebede295L); + private static final UUID EDDYSTONE_ADVANCED_REMAIN_CONNECTABLE_UUID = new UUID(0xA3C8750C8ed34bdfL, 0x8a39a01bebede295L); + + private BluetoothAdapter mAdapter; + private BluetoothDevice mBluetoothDevice; + private BluetoothGatt mBluetoothGatt; + private BluetoothGattCharacteristic mBroadcastCapabilitesCharacterisitc; + private BluetoothGattCharacteristic mRadioTxPowerCharacteristic; + private BluetoothGattCharacteristic mActiveSlotCharacteristic; + private BluetoothGattCharacteristic mAdvertisingIntervalCharacteristic; + private BluetoothGattCharacteristic mAdvancedAdvertisedTxPowerCharacteristic; + private BluetoothGattCharacteristic mLockStateCharacteristic; + private BluetoothGattCharacteristic mUnlockCharacteristic; + private BluetoothGattCharacteristic mPublicEcdhKeyCharacteristic; + private BluetoothGattCharacteristic mEidIdentityKeyCharacteristic; + private BluetoothGattCharacteristic mReadWriteAdvSlotCharacteristic; + private BluetoothGattCharacteristic mAdvancedFactoryResetCharacteristic; + private BluetoothGattCharacteristic mAdvancedRemainConnectableCharacteristic; + + private Handler mHandler; + private boolean mIsBeaconLocked = true; + private final Queue mQueue = new LinkedList(); + private boolean mConfigureSlot = false; + private boolean mReadlAllSlots = false; + private int mSlotCounter = 0; + + private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { + + int mLockState = 0x00; + + @Override + public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.v("BEACON", "Connection state change error: " + status); + broadcastError(status); + return; + } + + Log.v("BEACON", "Connection state change: " + newState); + + if (newState == BluetoothProfile.STATE_CONNECTED) { + setState(STATE_DISCOVERING_SERVICES); + mActiveSlotsTypes.clear(); + // Attempts to discover services after successful connection. + Log.v("BEACON", "delaying service discovery for 2s"); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + gatt.discoverServices(); + } + }, 2500); + Log.v("BEACON", "Calling gatt.discoverServies"); + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + mQueue.clear(); + mActiveSlotsTypes.clear(); + setState(STATE_DISCONNECTED); + refreshDeviceCache(gatt); + if(gatt != null) + gatt.close(); + mBluetoothGatt = null; + stopSelf(); + } + } + + @Override + public void onServicesDiscovered(final BluetoothGatt gatt, final int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.v("BEACON", "Service discovery error: " + status); + broadcastError(status); + return; + } + Log.v("BEACON", "onServices Discovered"); + // We have successfully connected + setState(STATE_CONNECTED); + + // Search for config service + final BluetoothGattService configService = gatt.getService(EDDYSTONE_GATT_CONFIG_SERVICE_UUID); + if (configService == null) { + // Config service is not present + Log.v("BEACON", "Gatt error on service discovery: " + ERROR_UNSUPPORTED_DEVICE); + broadcastError(ERROR_UNSUPPORTED_DEVICE); + setState(STATE_DISCONNECTING); + gatt.disconnect(); + return; + } + + mBroadcastCapabilitesCharacterisitc = configService.getCharacteristic(EDDYSTONE_BROADCAST_CAPABILITIES_UUID); + mRadioTxPowerCharacteristic = configService.getCharacteristic(EDDYSTONE_RADIO_TX_POWER_UUID); + mActiveSlotCharacteristic = configService.getCharacteristic(EDDYSTONE_ACTIVE_SLOT_UUID); + mAdvertisingIntervalCharacteristic = configService.getCharacteristic(EDDYSTONE_ADVERTISING_INTERVAL_UUID); + mAdvancedAdvertisedTxPowerCharacteristic = configService.getCharacteristic(EDDYSTONE_ADVANCED_ADVERTISED_TX_POWER_UUID); + mLockStateCharacteristic = configService.getCharacteristic(EDDYSTONE_LOCK_STATE_UUID); + mUnlockCharacteristic = configService.getCharacteristic(EDDYSTONE_UNLOCK_UUID); + mPublicEcdhKeyCharacteristic = configService.getCharacteristic(EDDYSTONE_ECDH_KEY_UUID); + mEidIdentityKeyCharacteristic = configService.getCharacteristic(EDDYSTONE_EID_IDENTITY_KEY_UUID); + mReadWriteAdvSlotCharacteristic = configService.getCharacteristic(EDDYSTONE_READ_WRITE_ADV_SLOT_UUID); + mAdvancedFactoryResetCharacteristic = configService.getCharacteristic(EDDYSTONE_ADVANCED_FACTORY_RESET_UUID); + mAdvancedRemainConnectableCharacteristic = configService.getCharacteristic(EDDYSTONE_ADVANCED_REMAIN_CONNECTABLE_UUID); + Log.v("BEACON", "Service discovery complete"); + if (mIsBeaconLocked) { + Log.v("BEACON", "Beacon Locked: " + mIsBeaconLocked); + add(RequestType.READ_CHARACTERISTIC, mUnlockCharacteristic); + Log.v("BEACON", "Read Challenge and queue size: " + mQueue.size()); + } + } + + @Override + public void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.v("BEACON", "Characteristic write error: " + status); + broadcastError(status); + return; + } + + if (EDDYSTONE_ACTIVE_SLOT_UUID.equals(characteristic.getUuid())) { + if(mReadlAllSlots) + add(RequestType.READ_CHARACTERISTIC, mActiveSlotCharacteristic); + else if (!mStartReadingInitialCharacteristics) { + add(RequestType.READ_CHARACTERISTIC, mActiveSlotCharacteristic); + startReadingCharacteristicsForActiveSlot(); + } + else { + mStartReadingInitialCharacteristics = false; + add(RequestType.READ_CHARACTERISTIC, mActiveSlotCharacteristic); + } + } else if (EDDYSTONE_ADVERTISING_INTERVAL_UUID.equals(characteristic.getUuid())) { + add(RequestType.READ_CHARACTERISTIC, mAdvertisingIntervalCharacteristic); + } else if (EDDYSTONE_RADIO_TX_POWER_UUID.equals(characteristic.getUuid())) { + add(RequestType.READ_CHARACTERISTIC, mRadioTxPowerCharacteristic); + } else if (mAdvancedAdvertisedTxPowerCharacteristic != null && EDDYSTONE_ADVANCED_ADVERTISED_TX_POWER_UUID.equals(characteristic.getUuid())) { + add(RequestType.READ_CHARACTERISTIC, mAdvancedAdvertisedTxPowerCharacteristic); + } else if (EDDYSTONE_LOCK_STATE_UUID.equals(characteristic.getUuid())) { + broadcastLockState(ParserUtils.getIntValue(characteristic.getValue(), 0, BluetoothGattCharacteristic.FORMAT_UINT8)); + } else if (EDDYSTONE_UNLOCK_UUID.equals(characteristic.getUuid())) { + if (mIsBeaconLocked) + add(RequestType.READ_CHARACTERISTIC, mLockStateCharacteristic); + } else if (EDDYSTONE_READ_WRITE_ADV_SLOT_UUID.equals(characteristic.getUuid())) { + startReadingCharacteristicsForActiveSlot(); + } else if (mAdvancedFactoryResetCharacteristic != null && EDDYSTONE_ADVANCED_FACTORY_RESET_UUID.equals(characteristic.getUuid())) { + broadcastAdvancedFactoryReset(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0)); + } else if (mAdvancedRemainConnectableCharacteristic != null && EDDYSTONE_ADVANCED_REMAIN_CONNECTABLE_UUID.equals(characteristic.getUuid())) { + add(RequestType.READ_CHARACTERISTIC, mAdvancedFactoryResetCharacteristic); + } + processNext(); + } + + @Override + public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + logw("Characteristic read error: " + status); + broadcastError(status); + return; + } + if (mIsBeaconLocked) { + if (EDDYSTONE_UNLOCK_UUID.equals(characteristic.getUuid())) { + Log.v("BEACON", "Broadcasting challenge"); + broadcastUnlockRequest(characteristic.getValue()); + processNext(); + return; + } else if (EDDYSTONE_LOCK_STATE_UUID.equals(characteristic.getUuid())) { + mLockState = ParserUtils.getIntValue(characteristic.getValue(), 0, BluetoothGattCharacteristic.FORMAT_UINT8); + switch (mLockState) { + case LOCKED: + mIsBeaconLocked = true; + processNext(); + return; + case UNLOCKED: + mIsBeaconLocked = false; + mLockState = UNLOCKED; + broadcastLockState(mLockState); + processNext(); + mReadlAllSlots = true; + add(RequestType.READ_CHARACTERISTIC, mBroadcastCapabilitesCharacterisitc); + return; + case UNLOCKED_AUTOMATIC_RELOCK_DISABLED: + mIsBeaconLocked = false; + mLockState = UNLOCKED_AUTOMATIC_RELOCK_DISABLED; + broadcastLockState(mLockState); + processNext(); + mReadlAllSlots = true; + add(RequestType.READ_CHARACTERISTIC, mBroadcastCapabilitesCharacterisitc); + return; + } + } + } + + if (EDDYSTONE_BROADCAST_CAPABILITIES_UUID.equals(characteristic.getUuid())) { + broadcastBeaconCapabilities(characteristic.getValue()); + startReadingAllActiveSlots(characteristic.getValue()); + } else if (EDDYSTONE_ACTIVE_SLOT_UUID.equals(characteristic.getUuid())) { + + if(mReadlAllSlots){ + mSlotCounter = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + if(mSlotCounter <= mMaxSlots - 1) { + add(RequestType.READ_CHARACTERISTIC, mReadWriteAdvSlotCharacteristic); + } else stopReadingAllActiveSlots(); + } else broadcastActiveSlot(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0)); + } else if (EDDYSTONE_ADVERTISING_INTERVAL_UUID.equals(characteristic.getUuid())) { + broadcastAdvertisingInterval(ParserUtils.getIntValue(characteristic.getValue(), 0, ParserUtils.FORMAT_UINT16_BIG_INDIAN)); + } else if (EDDYSTONE_RADIO_TX_POWER_UUID.equals(characteristic.getUuid())) { + broadcastRadioTxPower(characteristic.getValue()); + } else if (EDDYSTONE_ADVANCED_ADVERTISED_TX_POWER_UUID.equals(characteristic.getUuid())) { + broadcastAdvancedAdvertisedTxPower(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0)); + } else if (EDDYSTONE_LOCK_STATE_UUID.equals(characteristic.getUuid())) { + Log.v("Beacon", "Lock state: " + ParserUtils.getIntValue(characteristic.getValue(), 0, BluetoothGattCharacteristic.FORMAT_UINT8)); + broadcastLockState(mLockState); + } else if (EDDYSTONE_UNLOCK_UUID.equals(characteristic.getUuid())) { + } else if (EDDYSTONE_ECDH_KEY_UUID.equals(characteristic.getUuid())) { + broadcastEcdhKey(characteristic.getValue()); + } else if (EDDYSTONE_EID_IDENTITY_KEY_UUID.equals(characteristic.getUuid())) { + broadcastEidIdentityKey(characteristic.getValue()); + } else if (EDDYSTONE_READ_WRITE_ADV_SLOT_UUID.equals(characteristic.getUuid())) { + broadcastReadWriteAdvSlot(characteristic.getValue()); + } else if (EDDYSTONE_ADVANCED_REMAIN_CONNECTABLE_UUID.equals(characteristic.getUuid())) { + broadcastAdvancedRemainConnectable(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0)); + } + processNext(); + } + }; + + private int mMaxSlots = -1; + private int mMaxEidSlots = -1; + private ArrayList mActiveSlotsTypes = new ArrayList<>(); + private boolean mStartReadingInitialCharacteristics = false; + + public class ServiceBinder extends Binder { + + public byte[] mBeaconLockCode; + + /** + * Connects to the service. The bluetooth device must have been passed during binding to the service in {@link UpdateService#EXTRA_DATA} field. + * + * @return true if connection process has been initiated + */ + public boolean connect() { + if (mAdapter == null) { + logw("BluetoothAdapter not initialized or unspecified address."); + return false; + } + + if (mBluetoothDevice == null) { + logw("Target device not specified. Start service with the BluetoothDevice set in EXTRA_DATA field."); + return false; + } + + // the device may be already connected + if (mConnectionState == STATE_CONNECTED) { + return true; + } + + setState(STATE_CONNECTING); + mBluetoothGatt = mBluetoothDevice.connectGatt(UpdateService.this, false, mGattCallback); + return true; + } + + /** + * Disconnects from the device and closes the Bluetooth GATT object afterwards. + */ + public void disconnectAndClose() { + // This sometimes happen when called from UpdateService.ACTION_GATT_ERROR event receiver in UpdateFragment. + if (mBluetoothGatt == null) + return; + + setState(STATE_DISCONNECTING); + mBluetoothGatt.disconnect(); + + // Sometimes the connection gets error 129 or 133. Calling disconnect() method does not really disconnect... sometimes the connection is already broken. + // Here we have a security check that notifies UI about disconnection even if onConnectionStateChange(...) has not been called. + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (mConnectionState == STATE_DISCONNECTING) + mGattCallback.onConnectionStateChange(mBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_DISCONNECTED); + } + }, 1500); + } + + /** + * Reads all the values from the device, one by one. + * + * @return true if at least one required characteristic has been found on the beacon. + */ + public boolean read() { + if (mBluetoothGatt == null) + return false; + + if (mBroadcastCapabilitesCharacterisitc != null) { + mBluetoothGatt.readCharacteristic(mBroadcastCapabilitesCharacterisitc); + return true; + } else if (mActiveSlotCharacteristic != null) { + mBluetoothGatt.readCharacteristic(mActiveSlotCharacteristic); + return true; + } else if (mAdvertisingIntervalCharacteristic != null) { + mBluetoothGatt.readCharacteristic(mAdvertisingIntervalCharacteristic); + return true; + } + return false; + } + + /** + * Returns true if the beacon supports the advanced configuration. + */ + public boolean isAdvancedSupported() { + return mAdvancedAdvertisedTxPowerCharacteristic != null || mLockStateCharacteristic != null || mUnlockCharacteristic != null; + } + + public int getState() { + return mConnectionState; + } + + //Unlocking the beacon by writing to the unclock characteristic which is called when a client connects to the beacon's configuration service + public void unlockBeacon(final byte[] encryptedLockCode, final byte[] beaconLockCode) { + mBeaconLockCode = beaconLockCode; + if(mUnlockCharacteristic.setValue(encryptedLockCode)) { + add(RequestType.WRITE_CHARACTERISTIC, mUnlockCharacteristic); + } + } + + //Changing active slot to the selected active slot + public void changeToSelectedActiveSlot(final byte [] position) { + if(mActiveSlotCharacteristic.setValue(position)) { + add(RequestType.WRITE_CHARACTERISTIC, mActiveSlotCharacteristic); + } + } + + public void configureActiveSlot(final byte [] newSlotData, final String frameType) { + if(mReadWriteAdvSlotCharacteristic.setValue(newSlotData)){ + mConfigureSlot = true; + final int activeSlot = getActiveSlot(); + mActiveSlotsTypes.set(activeSlot, frameType); //Update the slotList on new slot configuration + add(RequestType.WRITE_CHARACTERISTIC, mReadWriteAdvSlotCharacteristic); + } + } + + public void configureRadioTxPower(byte[] radioTxPower) { + if(mRadioTxPowerCharacteristic.setValue(radioTxPower)){ + add(RequestType.WRITE_CHARACTERISTIC, mRadioTxPowerCharacteristic); + } + } + + public void configureAdvancedAdvertisedTxPower(final byte[] radioTxPower) { + if(mAdvancedAdvertisedTxPowerCharacteristic.setValue(radioTxPower)){ + add(RequestType.WRITE_CHARACTERISTIC, mAdvancedAdvertisedTxPowerCharacteristic); + } + } + public void configureAdvertistingInterval(byte[] advertisingInterval) { + if(mAdvertisingIntervalCharacteristic.setValue(advertisingInterval)) + add(RequestType.WRITE_CHARACTERISTIC, mAdvertisingIntervalCharacteristic); + } + + public void lockBeacon(byte[] lockCode) { + if(mLockStateCharacteristic.setValue(lockCode)){ + add(RequestType.WRITE_CHARACTERISTIC, mLockStateCharacteristic); + } + } + + public void startReadingCharacteristicsForActiveSlot() { + add(RequestType.READ_CHARACTERISTIC, mAdvertisingIntervalCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mRadioTxPowerCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mReadWriteAdvSlotCharacteristic); + } + + public void startReadingInitialCharacteristicsSet(){ + add(RequestType.READ_CHARACTERISTIC, mBroadcastCapabilitesCharacterisitc); + add(RequestType.READ_CHARACTERISTIC, mActiveSlotCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mAdvertisingIntervalCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mRadioTxPowerCharacteristic); + if(mAdvancedAdvertisedTxPowerCharacteristic != null) + add(RequestType.READ_CHARACTERISTIC, mAdvancedAdvertisedTxPowerCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mLockStateCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mReadWriteAdvSlotCharacteristic); + if(mAdvancedRemainConnectableCharacteristic != null) + add(RequestType.READ_CHARACTERISTIC, mAdvancedRemainConnectableCharacteristic); + } + + public byte [] getBroadcastCapabilities(){ + final BluetoothGattCharacteristic characteristic = mBroadcastCapabilitesCharacterisitc; + if(characteristic != null){ + final byte [] data = characteristic.getValue(); + if(data == null || data.length < 7){ + return null; + } + return data; + } + return null; + } + + /** + * Obtains the cached value of the active slot characteristic. If the value has not been obtained yet using {@link #read()}, or the characteristic has not been found on the beacon, + * -1 is returned. + * + * @return the advertising interval or -1 + */ + public int getActiveSlot() { + final BluetoothGattCharacteristic characteristic = mActiveSlotCharacteristic; + if(characteristic != null){ + final byte [] data = characteristic.getValue(); + if(data == null || data.length == 0){ + return -1; + } + return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + } + return -1; + } + + /** + * Obtains the cached value of the advertising interval characteristic. If the value has not been obtained yet using {@link #read()}, or the characteristic has not been found on the beacon, + * null is returned. + * + * @return the advertising interval or null + */ + public Integer getAdvInterval() { + final BluetoothGattCharacteristic characteristic = mAdvertisingIntervalCharacteristic; + if (characteristic != null) { + final byte[] data = characteristic.getValue(); + if (data == null || data.length == 0) + return null; + return ParserUtils.getIntValue(characteristic.getValue(), 0, ParserUtils.FORMAT_UINT16_BIG_INDIAN); + } + return null; + } + + /** + * Obtains the cached value of the Radio Tx Power characteristic. If the value has not been obtained yet using {@link #read()}, or the characteristic has not been found on the beacon, + * null is returned. + * + * @return the Radio Tx Power or null + */ + public Integer getRadioTxPower() { + final BluetoothGattCharacteristic characteristic = mRadioTxPowerCharacteristic; + if (characteristic != null) { + final byte[] data = characteristic.getValue(); + if (data == null || data.length == 0) + return null; + return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + } + return null; + } + + /** + * Obtains the cached value of the Advanced Advertised Tx Power characteristic. If the value has not been obtained yet using {@link #read()}, or the characteristic has not been found on the beacon, + * null is returned. + * + * @return the Advanced Advertised Tx Power or null + */ + public Integer getAdvancedAdvertisedTxPower() { + if(mAdvancedAdvertisedTxPowerCharacteristic == null) + return null; + + final BluetoothGattCharacteristic characteristic = mAdvancedAdvertisedTxPowerCharacteristic; + if (characteristic != null) { + final byte[] data = characteristic.getValue(); + if (data == null || data.length == 0) + return null; + return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + } + return null; + } + + /** + * Obtains the cached value of the Lock State characteristic. If the value has not been obtained yet using {@link #read()}, or the characteristic has not been found on the beacon, + * null is returned. + * + * @return the Lock State or null + */ + public Integer getLockState() { + final BluetoothGattCharacteristic characteristic = mLockStateCharacteristic; + if (characteristic != null) { + final byte[] data = characteristic.getValue(); + if (data == null || data.length == 0) + return null; + return ParserUtils.getIntValue(characteristic.getValue(), 0, BluetoothGattCharacteristic.FORMAT_UINT8); + } + return null; + } + + /** + * Obtains the cached value of the Public ECDH Key characteristic. If the value has not been obtained yet using {@link #read()}, or the characteristic has not been found on the beacon, + * null is returned. + * + * @return the Public ECDH Key or null + */ + public byte[] getBeaconPublicEcdhKey() { + final BluetoothGattCharacteristic characteristic = mPublicEcdhKeyCharacteristic; + if (characteristic != null) { + final byte[] data = characteristic.getValue(); + if (data == null || data.length == 0) + return null; + return characteristic.getValue(); + } + return null; + } + + /** + * Obtains the cached value of the Encrypted Identity Key characteristic. If the value has not been obtained yet using {@link #read()}, or the characteristic has not been found on the beacon, + * null is returned. + * + * @return the Encrypted Identity Key or null + */ + public byte[] getIdentityKey() { + final BluetoothGattCharacteristic characteristic = mEidIdentityKeyCharacteristic; + if (characteristic != null) { + final byte[] data = characteristic.getValue(); + if (data == null || data.length == 0) + return null; + return characteristic.getValue(); + } + return null; + } + + /** + * Obtains the cached value of the Read Write Adv Slot characteristic. If the value has not been obtained yet using {@link #read()}, or the characteristic has not been found on the beacon, + * null is returned. + * + * @return the Read Write Adv Slot or null + */ + public byte[] getReadWriteAdvSlotData() { + final BluetoothGattCharacteristic characteristic = mReadWriteAdvSlotCharacteristic; + if (characteristic != null) { + final byte[] data = characteristic.getValue(); + if (data == null || data.length == 0) + return null; + return characteristic.getValue(); + } + return null; + } + + /** + * Get beacon lock code stored in the service + * + * @return the beacon lock code or null + */ + public byte[] getBeaconLockCode() { + if(mBeaconLockCode != null) + return mBeaconLockCode; + return null; + } + + /** + * Get beacon slot information stored in the service + * + * @return the beacon slot iformation or null + */ + public ArrayList getAllSlotInformation() { + return mActiveSlotsTypes; + } + } + + @Override + public void onCreate() { + super.onCreate(); + + initialize(); + mHandler = new Handler(); + mConnectionState = STATE_DISCONNECTED; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (mBluetoothGatt != null) + mBluetoothGatt.disconnect(); + mHandler = null; + mBluetoothDevice = null; + } + + @Override + public IBinder onBind(final Intent intent) { + return new ServiceBinder(); + } + + @Override + public boolean onUnbind(final Intent intent) { + // We want to allow rebinding + return true; + } + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + mBluetoothDevice = intent.getParcelableExtra(EXTRA_DATA); + return START_NOT_STICKY; + } + + /** + * Initializes a reference to the local Bluetooth adapter. + */ + private void initialize() { + final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); + mAdapter = bluetoothManager.getAdapter(); + } + + private void setState(final int state) { + mConnectionState = state; + final Intent intent = new Intent(ACTION_STATE_CHANGED); + intent.putExtra(EXTRA_DATA, state); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastUuid(final UUID uuid) { + final Intent intent = new Intent(ACTION_UUID_READY); + intent.putExtra(EXTRA_DATA, new ParcelUuid(uuid)); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastBeaconCapabilities(final byte [] broadcastCapabilities) { + final Intent intent = new Intent(ACTION_BROADCAST_CAPABILITIES); + intent.putExtra(EXTRA_DATA, broadcastCapabilities); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastActiveSlot(final boolean activeSlot) { + final Intent intent = new Intent(ACTION_ACTIVE_SLOT); + if(activeSlot) + intent.putExtra(EXTRA_DATA, 0); + else + intent.putExtra(EXTRA_DATA, -1); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastActiveSlot(final int activeSlot) { + final Intent intent = new Intent(ACTION_ACTIVE_SLOT); + intent.putExtra(EXTRA_DATA, activeSlot); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastAdvertisingInterval(final int advertisingInterval) { + final Intent intent = new Intent(ACTION_ADVERTISING_INTERVAL); + intent.putExtra(EXTRA_DATA, advertisingInterval); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastRadioTxPower(final byte [] radioTxPower) { + final Intent intent = new Intent(ACTION_RADIO_TX_POWER); + intent.putExtra(EXTRA_DATA, radioTxPower); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastAdvancedAdvertisedTxPower(final int advertisedTxPower) { + final Intent intent = new Intent(ACTION_ADVANCED_ADVERTISED_TX_POWER); + intent.putExtra(EXTRA_DATA, advertisedTxPower); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastLockState(final int lockState) { + final Intent intent = new Intent(ACTION_LOCK_STATE); + intent.putExtra(EXTRA_DATA, lockState); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastUnlock(final byte [] unlock) { + final Intent intent = new Intent(ACTION_UNLOCK); + intent.putExtra(EXTRA_DATA, unlock); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastEcdhKey(final byte [] ecdhKey) { + final Intent intent = new Intent(ACTION_ECDH_KEY); + intent.putExtra(EXTRA_DATA, ecdhKey); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastEidIdentityKey(final byte [] identityKey) { + final Intent intent = new Intent(ACTION_EID_IDENTITY_KEY); + intent.putExtra(EXTRA_DATA, identityKey); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastReadWriteAdvSlot(final byte [] readWriteAdvSlot) { + final Intent intent = new Intent(ACTION_READ_WRITE_ADV_SLOT); + if(readWriteAdvSlot == null || readWriteAdvSlot.length == 0){ + if(mReadlAllSlots) { + mActiveSlotsTypes.add("EMPTY"); + mSlotCounter = mSlotCounter + 1 ; + if(mSlotCounter <= mMaxSlots - 1 ) { + if (mActiveSlotCharacteristic.setValue(new byte[]{(byte) mSlotCounter})) + add(RequestType.WRITE_CHARACTERISTIC, mActiveSlotCharacteristic); + } else stopReadingAllActiveSlots(); + } else { + intent.putExtra(EXTRA_FRAME_TYPE, EMPTY_SLOT); + intent.putExtra(EXTRA_DATA, readWriteAdvSlot); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + return; + } + final int frameType = ParserUtils.getIntValue(readWriteAdvSlot, 0, BluetoothGattCharacteristic.FORMAT_UINT8); + + switch (frameType){ + case TYPE_UID: + if(mReadlAllSlots) { + mActiveSlotsTypes.add("UID"); + mSlotCounter = mSlotCounter + 1 ; + if(mSlotCounter <= mMaxSlots - 1 ) { + if (mActiveSlotCharacteristic.setValue(new byte[]{(byte) mSlotCounter})) + add(RequestType.WRITE_CHARACTERISTIC, mActiveSlotCharacteristic); + } else stopReadingAllActiveSlots(); + return; + } + intent.putExtra(EXTRA_FRAME_TYPE, frameType); + intent.putExtra(EXTRA_NAMESPACE_ID, ParserUtils.bytesToHex(readWriteAdvSlot, 2, 10, true)); + intent.putExtra(EXTRA_INSTANCE_ID, ParserUtils.bytesToHex(readWriteAdvSlot, 12, 6, true)); + intent.putExtra(EXTRA_DATA, readWriteAdvSlot); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + break; + case TYPE_URL: + if(mReadlAllSlots) { + mActiveSlotsTypes.add("URL"); + mSlotCounter = mSlotCounter + 1 ; + if(mSlotCounter <= mMaxSlots - 1 ) { + if (mActiveSlotCharacteristic.setValue(new byte[]{(byte) mSlotCounter})) + add(RequestType.WRITE_CHARACTERISTIC, mActiveSlotCharacteristic); + } else stopReadingAllActiveSlots(); + return; + } + + intent.putExtra(EXTRA_FRAME_TYPE, frameType); + intent.putExtra(EXTRA_URL, ParserUtils.decodeUri(readWriteAdvSlot, 2, readWriteAdvSlot.length-2)); + intent.putExtra(EXTRA_DATA, readWriteAdvSlot); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + break; + case TYPE_TLM: + if(mReadlAllSlots) { + mActiveSlotsTypes.add("TLM"); + mSlotCounter = mSlotCounter + 1 ; + if(mSlotCounter <= mMaxSlots - 1 ) { + if (mActiveSlotCharacteristic.setValue(new byte[]{(byte) mSlotCounter})) + add(RequestType.WRITE_CHARACTERISTIC, mActiveSlotCharacteristic); + } else stopReadingAllActiveSlots(); + return; + } + + intent.putExtra(EXTRA_FRAME_TYPE, frameType); + if(mActiveSlotsTypes.contains("EID")){ + intent.putExtra(EXTRA_ETLM, ParserUtils.bytesToHex(readWriteAdvSlot, 2, 12, true)); + intent.putExtra(EXTRA_SALT, ParserUtils.bytesToHex(readWriteAdvSlot, 14, 2, true)); + intent.putExtra(EXTRA_MESSAGE_INTEGRITY_CHECK, ParserUtils.bytesToHex(readWriteAdvSlot, 16, 2, true)); + intent.putExtra(EXTRA_DATA, readWriteAdvSlot); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + return; + } + + final int voltage = ParserUtils.decodeUint16BigEndian(readWriteAdvSlot, 2); + if(voltage > 0){ + intent.putExtra(EXTRA_VOLTAGE, String.valueOf(voltage) + getString(R.string.voltage_unit)); + } else { + intent.putExtra(EXTRA_VOLTAGE, getString(R.string.batt_voltage_unsupported)); + } + + final float temp = ParserUtils.decode88FixedPointNotation(readWriteAdvSlot, 4); + if (temp > -128.0f) + intent.putExtra(EXTRA_BEACON_TEMPERATURE, String.valueOf(temp) + getString(R.string.temperature_unit)); + else + intent.putExtra(EXTRA_BEACON_TEMPERATURE, getString(R.string.temperature_unsupported)); + + intent.putExtra(EXTRA_PDU_COUNT, String.valueOf(ParserUtils.decodeUint32BigEndian(readWriteAdvSlot, 6))); + intent.putExtra(EXTRA_TIME_SINCE_BOOT, String.valueOf(ParserUtils.decodeUint32BigEndian(readWriteAdvSlot, 10) * 100)); + intent.putExtra(EXTRA_DATA, readWriteAdvSlot); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + break; + case TYPE_EID: + if(mReadlAllSlots) { + mActiveSlotsTypes.add("EID"); + mSlotCounter = mSlotCounter + 1 ; + if(mSlotCounter <= mMaxSlots - 1 ) + if (mActiveSlotCharacteristic.setValue(new byte[]{(byte) mSlotCounter})) + add(RequestType.WRITE_CHARACTERISTIC, mActiveSlotCharacteristic); + else stopReadingAllActiveSlots(); + return; + } + add(RequestType.READ_CHARACTERISTIC, mPublicEcdhKeyCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mEidIdentityKeyCharacteristic); + intent.putExtra(EXTRA_FRAME_TYPE, frameType); + intent.putExtra(EXTRA_TIMER_EXPONENT, String.valueOf(ParserUtils.getIntValue(readWriteAdvSlot, 1, BluetoothGattCharacteristic.FORMAT_UINT8))); + intent.putExtra(EXTRA_CLOCK_VALUE, String.valueOf(ParserUtils.getIntValue(readWriteAdvSlot, 2, ParserUtils.FORMAT_UINT32_BIG_INDIAN))); + intent.putExtra(EXTRA_EID, String.valueOf(ParserUtils.bytesToHex(readWriteAdvSlot, 6, 8, true))); + intent.putExtra(EXTRA_DATA, readWriteAdvSlot); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + break; + } + } + + private void broadcastAdvancedFactoryReset(final int factoryReset) { + final Intent intent = new Intent(ACTION_ADVANCED_FACTORY_RESET); + intent.putExtra(EXTRA_DATA, factoryReset); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastAdvancedRemainConnectable(final int remainConnectable) { + final Intent intent = new Intent(ACTION_ADVANCED_REMAIN_CONNECTABLE); + if(remainConnectable > 0) + intent.putExtra(EXTRA_DATA, true); + else + intent.putExtra(EXTRA_DATA, false); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastError(final int error) { + final Intent intent = new Intent(ACTION_GATT_ERROR); + intent.putExtra(EXTRA_DATA, error); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastUnlockRequest(final byte [] challenge) { + final Intent intent = new Intent(ACTION_UNLOCK_BEACON); + intent.putExtra(EXTRA_DATA, challenge); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void broadcastAllSlotInformation(){ + final Intent intent = new Intent(ACTION_BROADCAST_ALL_SLOT_INFO); + intent.putStringArrayListExtra(EXTRA_DATA, mActiveSlotsTypes); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void startReadingAllActiveSlots(byte [] mBroadcastCapabilities) { + if (mMaxSlots < 0) + mMaxSlots = ParserUtils.getIntValue(mBroadcastCapabilities, 1, BluetoothGattCharacteristic.FORMAT_UINT8); + if (mMaxEidSlots < 0) + mMaxEidSlots = ParserUtils.getIntValue(mBroadcastCapabilities, 2, BluetoothGattCharacteristic.FORMAT_UINT8); + if (mSlotCounter < mMaxSlots) + add(RequestType.READ_CHARACTERISTIC, mActiveSlotCharacteristic); + } + + private void stopReadingAllActiveSlots() { + mReadlAllSlots = false; + mSlotCounter = 0; + mStartReadingInitialCharacteristics = true; + if(mActiveSlotCharacteristic.setValue(new byte [] {(byte) mSlotCounter})) { + add(RequestType.WRITE_CHARACTERISTIC, mActiveSlotCharacteristic); + startReadingCharacteristicsForActiveSlot(); + } + } + + private void startReadingInitialCharacteristicsSet() { + add(RequestType.READ_CHARACTERISTIC, mBroadcastCapabilitesCharacterisitc); + if(!mReadlAllSlots) { + add(RequestType.READ_CHARACTERISTIC, mActiveSlotCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mAdvertisingIntervalCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mRadioTxPowerCharacteristic); + if (mAdvancedAdvertisedTxPowerCharacteristic != null) + add(RequestType.READ_CHARACTERISTIC, mAdvancedAdvertisedTxPowerCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mLockStateCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mReadWriteAdvSlotCharacteristic); + if (mAdvancedRemainConnectableCharacteristic != null) + add(RequestType.READ_CHARACTERISTIC, mAdvancedRemainConnectableCharacteristic); + } + } + + private void startReadingCharacteristicsForActiveSlot() { + add(RequestType.READ_CHARACTERISTIC, mAdvertisingIntervalCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mRadioTxPowerCharacteristic); + if (mAdvancedAdvertisedTxPowerCharacteristic != null) + add(RequestType.READ_CHARACTERISTIC, mAdvancedAdvertisedTxPowerCharacteristic); + add(RequestType.READ_CHARACTERISTIC, mReadWriteAdvSlotCharacteristic); + if (mAdvancedRemainConnectableCharacteristic != null) + add(RequestType.READ_CHARACTERISTIC, mAdvancedRemainConnectableCharacteristic); + broadcastAllSlotInformation(); + } + + /** + * Clears the device cache. + *

+ * CAUTION:
+ * It is very unsafe to call the refresh() method. First of all it's hidden so it may be removed in the future release of Android. We do it because Nordic Beacon may advertise as a beacon, as + * Beacon Config or DFU. Android does not clear cache then device is disconnected unless manually restarted Bluetooth Adapter. To do this in the code we need to call + * {@link BluetoothGatt#refresh()} method. However is may cause a lot of troubles. Ideally it should be called before connection attempt but we get 'gatt' object by calling connectGatt method so + * when the connection already has been started. Calling refresh() afterwards causes errors 129 and 133 to pop up from time to time when refresh takes place actually during service discovery. It + * seems to be asynchronous method. Therefore we are refreshing the device after disconnecting from it, before closing gatt. Sometimes you may obtain services from cache, not the actual values so + * reconnection is required. + * + * @param gatt + * the Bluetooth GATT object to refresh. + */ + private boolean refreshDeviceCache(final BluetoothGatt gatt) { + /* + * There is a refresh() method in BluetoothGatt class but for now it's hidden. We will call it using reflections. + */ + try { + final Method refresh = gatt.getClass().getMethod("refresh"); + if (refresh != null) { + return (Boolean) refresh.invoke(gatt); + } + } catch (final Exception e) { + loge("An exception occurred while refreshing device"); + } + return false; + } + + private void loge(final String message) { + if (BuildConfig.DEBUG) + Log.e(TAG, message); + } + + private void logw(final String message) { + if (BuildConfig.DEBUG) + Log.w(TAG, message); + } + + public static int decodeUInt16(final BluetoothGattCharacteristic characteristic, final int offset) { + final byte[] data = characteristic.getValue(); + return (unsignedByteToInt(data[offset]) << 8) | unsignedByteToInt(data[offset + 1]); + } + + public static UUID decodeBeaconUUID(final BluetoothGattCharacteristic characteristic) { + final byte[] data = characteristic.getValue(); + final long mostSigBits = (unsignedByteToLong(data[0]) << 56) + (unsignedByteToLong(data[1]) << 48) + (unsignedByteToLong(data[2]) << 40) + (unsignedByteToLong(data[3]) << 32) + + (unsignedByteToLong(data[4]) << 24) + (unsignedByteToLong(data[5]) << 16) + (unsignedByteToLong(data[6]) << 8) + unsignedByteToLong(data[7]); + final long leastSigBits = (unsignedByteToLong(data[8]) << 56) + (unsignedByteToLong(data[9]) << 48) + (unsignedByteToLong(data[10]) << 40) + (unsignedByteToLong(data[11]) << 32) + + (unsignedByteToLong(data[12]) << 24) + (unsignedByteToLong(data[13]) << 16) + (unsignedByteToLong(data[14]) << 8) + unsignedByteToLong(data[15]); + return new UUID(mostSigBits, leastSigBits); + } + + /** + * Convert a signed byte to an unsigned long. + */ + public static long unsignedByteToLong(byte b) { + return b & 0xFF; + } + + /** + * Convert a signed byte to an unsigned int. + */ + public static int unsignedByteToInt(int b) { + return b & 0xFF; + } + + /** + * BluetoothGatt request types. + */ + public enum RequestType { + // CHARACTERISTIC_NOTIFICATION, + READ_CHARACTERISTIC, + READ_DESCRIPTOR, + // READ_RSSI, + WRITE_CHARACTERISTIC, + WRITE_DESCRIPTOR + } + + public void add(RequestType type, BluetoothGattDescriptor descriptor) { + Request request = new Request(type, descriptor); + add(request); + } + + public void add(RequestType type, BluetoothGattCharacteristic characteristic) { + Request request = new Request(type, characteristic); + add(request); + } + + synchronized private void add(Request request) { + mQueue.add(request); + if (mQueue.size() == 1) { + mQueue.peek().start(mBluetoothGatt); + } + } + + /** + * Process the next request in the queue for a BluetoothGatt function (such as characteristic read). + */ + synchronized private void processNext() { + // The currently executing request is kept on the head of the queue until this is called. + if (mQueue.isEmpty()) + throw new RuntimeException("No active request in processNext()"); + mQueue.remove(); + if (!mQueue.isEmpty()) { + mQueue.peek().start(mBluetoothGatt); + } + } + + /** + * The object that holds a Gatt request while in the queue. + *
+ * This object holds the parameters for calling BluetoothGatt methods (see start()); + */ + public class Request { + final RequestType requestType; + BluetoothGattCharacteristic characteristic; + BluetoothGattDescriptor descriptor; + + public Request(RequestType requestType, BluetoothGattCharacteristic characteristic) { + this.requestType = requestType; + this.characteristic = characteristic; + } + + public Request(RequestType requestType, BluetoothGattDescriptor descriptor) { + this.requestType = requestType; + this.descriptor = descriptor; + } + + public void start(BluetoothGatt bluetoothGatt) { + switch (requestType) { + case READ_CHARACTERISTIC: + if (!bluetoothGatt.readCharacteristic(characteristic)) { + throw new IllegalArgumentException("Characteristic is not valid: " + characteristic.getUuid().toString()); + } + break; + case READ_DESCRIPTOR: + if (!bluetoothGatt.readDescriptor(descriptor)) { + throw new IllegalArgumentException("Descriptor is not valid"); + } + break; + case WRITE_CHARACTERISTIC: + if (!bluetoothGatt.writeCharacteristic(characteristic)) { + throw new IllegalArgumentException("Characteristic is not valid"); + } + break; + case WRITE_DESCRIPTOR: + if (!bluetoothGatt.writeDescriptor(descriptor)) { + throw new IllegalArgumentException("Characteristic is not valid"); + } + break; + } + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/beacon/BeaconsFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/beacon/BeaconsFragment.java new file mode 100644 index 0000000..b2ccefa --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/beacon/BeaconsFragment.java @@ -0,0 +1,632 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, getActivity() list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, getActivity() list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from getActivity() + * software without specific prior written permission. + * + * getActivity() SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF getActivity() SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.beacon; + +import android.Manifest; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Activity; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.common.AccountPicker; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.nearby.Nearby; +import com.google.android.gms.nearby.messages.Message; +import com.google.android.gms.nearby.messages.MessageFilter; +import com.google.android.gms.nearby.messages.MessageListener; +import com.google.android.gms.nearby.messages.NearbyMessagesStatusCodes; +import com.google.android.gms.nearby.messages.Strategy; +import com.google.android.gms.nearby.messages.SubscribeOptions; + +import java.nio.charset.Charset; +import java.util.ArrayList; + +import no.nordicsemi.android.nrfbeacon.nearby.AuthorizedServiceTask; +import no.nordicsemi.android.nrfbeacon.nearby.EddystoneBeaconsAdapter; +import no.nordicsemi.android.nrfbeacon.nearby.MainActivity; +import no.nordicsemi.android.nrfbeacon.nearby.NearbyBackgroundService; +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.common.BaseFragment; +import no.nordicsemi.android.nrfbeacon.nearby.common.PermissionRationaleDialogFragment; +import no.nordicsemi.android.nrfbeacon.nearby.settings.NearbySettingsActivity; +import no.nordicsemi.android.nrfbeacon.nearby.util.NetworkUtils; + +public class BeaconsFragment extends BaseFragment implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, PermissionRationaleDialogFragment.PermissionDialogListener { + + public static final String EXTRA_ADAPTER_POSITION = "no.nordicsemi.android.nrfbeacon.extra.adapter_position"; + public static final String TAG = "BEACON"; + private static final String ACCOUNT_NAME_PREF = "userAccount"; + private static final String SHARED_PREFS_NAME = "nrfNearbyInfo"; + private static final String NEARBY_MESSAGE = "no.nordicsemi.android.nrfbeacon.nearby.NEARBY_MESSAGE"; + public static final String NEARBY_DEVICE_DATA = "NEARBY_DEVICE_DATA"; + private static final String AUTH_PROXIMITY_API = "oauth2:https://www.googleapis.com/auth/userlocation.beacon.registry"; + + private final static int OPEN_ACTIVITY_REQ = 195; // random + static final int REQUEST_CODE_USER_ACCOUNT = 1002; + private static final int REQUEST_BACKGROUND_SCANNING = 252; + private static final int REQUEST_PERMISSION_REQ_CODE = 76; // any 8-bit number + private static final int REQUEST_ENABLE_BT = 1; + private static final int REQUEST_RESOLVE_ERROR = 261; //random + private static final int NOTIFICATION_ID = 1; + + private boolean mFragmentResumed; + public ArrayList mNearbyDevicesMessageList; + public ArrayList mTempList; + private int mSelectedTabPosition = 0; + //private Context mContext; + private EddystoneBeaconsAdapter mEddystoneBeaconsAdapter; + private ImageView mNearbyPermission; + private TextView mTextView; + private boolean mNearbyPermissionGranted = false; + private GoogleApiClient mGoogleApiClient; + private boolean mResolvingError; + private boolean mScanForNearbyInBackground = false; + private PendingIntent mPendingIntent; + private Intent mParentIntent; + private NotificationManagerCompat mNotificationManager; + private Context mContext; + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + } + + @Override + public void onAttach(final Context context) { + super.onAttach(context); + mContext = context; + + } + + @Override + public void onDestroy() { + super.onDestroy(); + final MainActivity parent = (MainActivity) getActivity(); + parent.setBeaconsFragment(null); + } + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + mSelectedTabPosition = getArguments().getInt("index"); + } + + if(savedInstanceState != null){ + mSelectedTabPosition = savedInstanceState.getInt(EXTRA_ADAPTER_POSITION); + } + + final MainActivity parent = (MainActivity) mContext; + parent.setBeaconsFragment(this); + } + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_beacons_list, container, false); + final ListView listView = (ListView) rootView.findViewById(R.id.listNearbyBeacons); + mNearbyDevicesMessageList = new ArrayList<>(); + mTempList = new ArrayList<>(); + mEddystoneBeaconsAdapter = new EddystoneBeaconsAdapter(getActivity(), mNearbyDevicesMessageList); + listView.setAdapter(mEddystoneBeaconsAdapter); + mTextView = (TextView) rootView.findViewById(R.id.tvNearbyPermission); + mNearbyPermission = (ImageView) rootView.findViewById(R.id.imageView); + + mParentIntent = new Intent(getActivity(), MainActivity.class); + mNotificationManager = NotificationManagerCompat.from(getActivity()); + mScanForNearbyInBackground = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(getString(R.string.nearby_settings_key), false); + mGoogleApiClient = new GoogleApiClient.Builder(getActivity()) + .addApi(Nearby.MESSAGES_API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + + return rootView; + } + + @Override + public void onViewCreated(final View view, final Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setHasOptionsMenu(true); + + mTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getNearbyPermissionStatusAndSubscribe(); + mTextView.setClickable(false); + } + }); + getNearbyPermissionStatusAndSubscribe(); + } + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + inflater.inflate(R.menu.menu_nearby_about, menu); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.action_nearby_settings: + Intent nearby_settings = new Intent(getActivity(), NearbySettingsActivity.class); + startActivityForResult(nearby_settings, REQUEST_BACKGROUND_SCANNING); + return true; + } + return false; + } + + @Override + public void onStart() { + super.onStart(); + final String [] permissions = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.GET_ACCOUNTS}; + ensurePermission(permissions); + } + + @Override + public void onStop() { + super.onStop(); + if(!mScanForNearbyInBackground){ + unsubscribe(); + disconnectFromGoogleApiClient(); + } + + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case REQUEST_BACKGROUND_SCANNING: + if (resultCode == Activity.RESULT_OK){ + boolean flag = Boolean.valueOf(data.getData().toString()); + Log.v(TAG, "background scanning enableD? " + flag); + updateNearbyScanning(); + } + break; + case REQUEST_ENABLE_BT: + if (resultCode == Activity.RESULT_OK) + connectToGoogleApiClient(); + else + onDestroy(); + break; + case REQUEST_CODE_USER_ACCOUNT: + if (resultCode == Activity.RESULT_OK) { + if (data != null) { + final String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); + setUserAccountName(accountName); + // The first time the account tries to contact the beacon service we'll pop a dialog + // asking the user to authorize our activity. Ensure that's handled cleanly here, rather + // than when the scan tries to fetch the status of every beacon within range. + Account [] accountsList = AccountManager.get(getActivity()).getAccounts(); + for (Account account : accountsList){ + if (accountName.equals(account.name)){ + if(NetworkUtils.checkNetworkConnectivity(getActivity())) + new AuthorizedServiceTask(getActivity(), account, AUTH_PROXIMITY_API).execute(); + else + Toast.makeText(getActivity(), getString(R.string.check_internet_connectivity), Toast.LENGTH_SHORT).show(); + break; + } + } + } + } + connectToGoogleApiClient(); + break; + case REQUEST_RESOLVE_ERROR: + if(resultCode == Activity.RESULT_OK) + getNearbyPermissionStatusAndSubscribe(); + else Toast.makeText(getActivity(), getString(R.string.rationale_permission_denied), Toast.LENGTH_SHORT).show(); + break; + + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case REQUEST_PERMISSION_REQ_CODE: { + if(permissions.length > 0) + for(int i = 0; i < permissions.length; i++){ + if(Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[i])){ + if(grantResults[i] == PackageManager.PERMISSION_GRANTED) + onPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION); + else Toast.makeText(getActivity(), R.string.rationale_permission_denied, Toast.LENGTH_SHORT).show(); + } else if (Manifest.permission.GET_ACCOUNTS.equals(permissions[i])){ + if(grantResults[i] == PackageManager.PERMISSION_GRANTED) + onPermissionGranted(Manifest.permission.GET_ACCOUNTS); + else Toast.makeText(getActivity(), R.string.rationale_permission_denied, Toast.LENGTH_SHORT).show(); + } + } + break; + } + } + } + + @Override + protected void onPermissionGranted(final String permission) { + // Now, when the permission is granted, we may start scanning for beacons. + // We bind even if the FAB was clicked. + if(Manifest.permission.ACCESS_COARSE_LOCATION.equalsIgnoreCase(permission)){ + if(!isBleEnabled()){ + enableBle(); + } else connectToGoogleApiClient(); + } else if (Manifest.permission.GET_ACCOUNTS.equalsIgnoreCase(permission)){ + selectUserAccount(); + } + } + + public void updateAdapter(boolean clearAdapter){ + if(clearAdapter) + mEddystoneBeaconsAdapter.clear(); + mEddystoneBeaconsAdapter.notifyDataSetChanged(); + } + + + + public void updateNearbyPermissionStatus(boolean flag){ + mNearbyPermissionGranted = flag; + if(!mNearbyPermissionGranted) { + mNearbyPermission.setVisibility(View.GONE); + mTextView.setVisibility(View.VISIBLE); + mTextView.setClickable(true); + } + else { + mNearbyPermission.setVisibility(View.VISIBLE); + mTextView.setVisibility(View.GONE); + } + } + + public void prepareForScanning(){ + final String [] permissions = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.GET_ACCOUNTS}; + if (ensurePermission(permissions)) { + if (!isBleEnabled()) { + enableBle(); + connectToGoogleApiClient(); + } else connectToGoogleApiClient(); + } + } + + /** + * Checks whether the Bluetooth adapter is enabled. + */ + private boolean isBleEnabled() { + final BluetoothManager bm = (BluetoothManager) getActivity().getSystemService(Context.BLUETOOTH_SERVICE); + final BluetoothAdapter ba = bm.getAdapter(); + return ba != null && ba.isEnabled(); + } + + /** + * Tries to start Bluetooth adapter. + */ + private void enableBle() { + final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableIntent, REQUEST_ENABLE_BT); + } + + private void selectUserAccount() { + String accountName = getUserAccountName(); + if (accountName == null) { + String[] accountTypes = new String[]{"com.google"}; + Intent intent = AccountPicker.newChooseAccountIntent( + null, null, accountTypes, false, null, null, null, null); + startActivityForResult(intent, REQUEST_CODE_USER_ACCOUNT); + } + } + + private String getUserAccountName(){ + SharedPreferences sharedPreferences = getActivity().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + return sharedPreferences.getString(ACCOUNT_NAME_PREF, null); + } + + private void setUserAccountName(final String accountName){ + SharedPreferences sharedPreferences = getActivity().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(ACCOUNT_NAME_PREF, accountName).apply(); + } + + private void connectToGoogleApiClient(){ + getNearbyPermissionStatusAndSubscribe(); + } + + @Override + public void onConnected(Bundle bundle) { + Log.v(TAG, "Connected "); + getNearbyPermissionStatusAndSubscribe(); + } + + @Override + public void onConnectionSuspended(int i) { + Log.v(TAG, "Connection susspended: " + i); + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + Log.v(TAG, "Connection failed: " + connectionResult.getErrorMessage()); + } + + public void getNearbyPermissionStatusAndSubscribe(){ + if(mGoogleApiClient != null && !mGoogleApiClient.isConnected()){ + if(!mGoogleApiClient.isConnecting()){ + mGoogleApiClient.connect(); + } + } else { + Nearby.Messages.getPermissionStatus(mGoogleApiClient).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull Status status) { + if (status.isSuccess()) { + updateNearbyPermissionStatus(true); + + subscribe(); + } else if (status.getStatusCode() == NearbyMessagesStatusCodes.APP_NOT_OPTED_IN) { + try { + status.startResolutionForResult(getActivity(), REQUEST_RESOLVE_ERROR); + } catch (IntentSender.SendIntentException e) { + mResolvingError = false; + Log.i(TAG, "Failed to resolve error status.", e); + } + } + } + }); + } + } + + private void subscribe() { + Log.v(TAG, "Subscribing to beacons"); + MessageFilter filter = new MessageFilter.Builder() + .includeNamespacedType("nrf-nearby-1100", "string") + .build(); + if (!mGoogleApiClient.isConnected()) { + if (!mGoogleApiClient.isConnecting()) { + mGoogleApiClient.connect(); + } + } else if (!mScanForNearbyInBackground) { + SubscribeOptions options = new SubscribeOptions.Builder().setStrategy(Strategy.BLE_ONLY).setFilter(filter).build(); + Nearby.Messages.subscribe(mGoogleApiClient, mMessageListener, options).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull Status status) { + if (status.isSuccess()) { + Log.i(TAG, "Subscribed successfully for foreground scanning."); + } else { + Log.i(TAG, "Could not subscribe."); + handleUnsuccessfulNearbyResult(status); + } + } + }); + + } else { + SubscribeOptions options = new SubscribeOptions.Builder().setStrategy(Strategy.BLE_ONLY).build(); + Nearby.Messages.subscribe(mGoogleApiClient, getPendingIntent(), options) + .setResultCallback(new ResultCallback() { + @Override + public void onResult(Status status) { + if (status.isSuccess()) { + Log.i(TAG, "Subscribed successfully for background scanning."); + } else { + Log.i(TAG, "Could not subscribe."); + handleUnsuccessfulNearbyResult(status); + } + } + }); + } + } + + private void unsubscribe(){ + if(mGoogleApiClient.isConnected()) { + Nearby.Messages.unsubscribe(mGoogleApiClient, mMessageListener); + } + } + + private void disconnectFromGoogleApiClient(){ + + if(mGoogleApiClient.isConnected()) { + unsubscribe(); + mGoogleApiClient.disconnect(); + Log.v(TAG, "is connected? " + mGoogleApiClient.isConnected()); + } + mNearbyDevicesMessageList.clear(); + mNotificationManager.cancelAll(); + } + + private PendingIntent getPendingIntent() { + PendingIntent p = PendingIntent.getService(getActivity(), 0, + getBackgroundSubscribeServiceIntent(), PendingIntent.FLAG_UPDATE_CURRENT); + return p; + } + + public Intent getBackgroundSubscribeServiceIntent() { + return new Intent(getActivity(), NearbyBackgroundService.class); + } + + private void handleUnsuccessfulNearbyResult(Status status) { + Log.i(TAG, "Processing error, status = " + status); + if (mResolvingError) { + // Already attempting to resolve an error. + return; + } else if (status.hasResolution()) { + try { + mResolvingError = true; + status.startResolutionForResult(getActivity(), + REQUEST_RESOLVE_ERROR); + } catch (IntentSender.SendIntentException e) { + mResolvingError = false; + Log.i(TAG, "Failed to resolve error status.", e); + } + } else { + if (status.getStatusCode() == CommonStatusCodes.NETWORK_ERROR) { + Toast.makeText(getActivity(), + "No connectivity, cannot proceed. Fix in 'Settings' and try again.", + Toast.LENGTH_LONG).show(); + } else { + // To keep things simple, pop a toast for all other error messages. + Toast.makeText(getActivity(), "Unsuccessful: " + + status.getStatusMessage(), Toast.LENGTH_LONG).show(); + } + } + } + + private final MessageListener mMessageListener = new MessageListener() { + @Override + public void onFound(Message message) { + String nearbyMessage = new String(message.getContent(), Charset.forName("UTF-8")); + Log.i(TAG, "Found message via PendingIntent: " + nearbyMessage); + displayNotification(message); + } + + @Override + public void onLost(Message message) { + String nearbyMessage = new String(message.getContent(), Charset.forName("UTF-8")); + Log.i(TAG, "Lost message via PendingIntent: " + nearbyMessage); + updateNotification(message); + } + }; + + private void displayNotification(Message message){ + if (!checkIfnearbyDeviceAlreadyExists(new String(message.getContent(), Charset.forName("UTF-8")))) { + Log.i(TAG, "Adding message"); + mNearbyDevicesMessageList.add(message); + updateAdapter(false); + Log.i(TAG, "count after adding: " + mNearbyDevicesMessageList.size()); + createNotification(); + } + } + + private void createNotification() { + + if(mNearbyDevicesMessageList.size() == 0){ + mNotificationManager.cancelAll(); + return; + } + + final ArrayList nearbyMessageList = loadNearbyMessageListForNotification(); + mParentIntent.putExtra(NEARBY_DEVICE_DATA, nearbyMessageList); + mParentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mPendingIntent = PendingIntent.getActivities(getActivity(), OPEN_ACTIVITY_REQ, new Intent[]{mParentIntent}, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(getActivity()) + .setSmallIcon(R.drawable.ic_eddystone) + .setColor(ContextCompat.getColor(getActivity(), R.color.actionBarColor)) + .setContentTitle(getString(R.string.app_name)) + .setContentIntent(mPendingIntent); + + if(mNearbyDevicesMessageList.size() == 1) { + mBuilder.setContentText(new String(mNearbyDevicesMessageList.get(0).getContent(), Charset.forName("UTF-8"))); + } else { + NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); + inboxStyle.setBigContentTitle(getString(R.string.app_name)); + inboxStyle.setSummaryText(mNearbyDevicesMessageList.size() + " beacons found"); + mBuilder.setContentText(mNearbyDevicesMessageList.size() + " beacons found"); + for (int i = 0; i < mNearbyDevicesMessageList.size(); i++) { + inboxStyle.addLine(new String(mNearbyDevicesMessageList.get(i).getContent(), Charset.forName("UTF-8"))); + } + mBuilder.setStyle(inboxStyle); + } + mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build()); + } + + private void updateNotification(Message message){ + if (checkIfnearbyDeviceAlreadyExists(new String(message.getContent(), Charset.forName("UTF-8")))) { + Log.i(TAG, "removing message: " + message); + removeLostNearbyMessage(new String(message.getContent(), Charset.forName("UTF-8"))); + Log.i(TAG, "count after removing: " + mNearbyDevicesMessageList.size()); + createNotification(); + } + } + + private ArrayList loadNearbyMessageListForNotification(){ + final ArrayList nearbyMessageList = new ArrayList<>(); + for(int i = 0; i < mNearbyDevicesMessageList.size(); i++){ + nearbyMessageList.add(mNearbyDevicesMessageList.get(i)); + } + return nearbyMessageList; + } + + private boolean checkIfnearbyDeviceAlreadyExists(String nearbyDeviceMessage){ + String message; + for(int i = 0; i < mNearbyDevicesMessageList.size(); i++){ + message = new String(mNearbyDevicesMessageList.get(i).getContent(), Charset.forName("UTF-8")); + if(nearbyDeviceMessage.equals(message)){ + return true; + } + } + return false; + } + + private void removeLostNearbyMessage(String nearbyDeviceMessage){ + String message; + for(int i = 0; i < mNearbyDevicesMessageList.size(); i++){ + message = new String(mNearbyDevicesMessageList.get(i).getContent(), Charset.forName("UTF-8")); + if(nearbyDeviceMessage.equals(message)){ + mNearbyDevicesMessageList.remove(i); + updateAdapter(false); + break; + } + } + } + + public void updateNearbyScanning(){ + boolean flag = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(getString(R.string.nearby_settings_key), true); + if(mScanForNearbyInBackground != flag){ + mScanForNearbyInBackground = flag; + unsubscribe(); + disconnectFromGoogleApiClient(); + mNotificationManager.cancelAll(); + mNearbyDevicesMessageList.clear(); + updateAdapter(true); + connectToGoogleApiClient(); + } else if(!mScanForNearbyInBackground && !flag){ + mNotificationManager.cancelAll(); + mNearbyDevicesMessageList.clear(); + updateAdapter(true); + connectToGoogleApiClient(); + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/BaseFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/BaseFragment.java new file mode 100644 index 0000000..1460480 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/BaseFragment.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrfbeacon.nearby.common; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.content.ContextCompat; + +import java.util.ArrayList; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + + +public abstract class BaseFragment extends Fragment implements PermissionRationaleDialogFragment.PermissionDialogListener { + private static final int REQUEST_PERMISSION_REQ_CODE = 76; // any 8-bit number + private ArrayList mPermissionList; + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onRequestPermission() { + if(mPermissionList != null && mPermissionList.size() > 0) + requestPermissions(mPermissionList.toArray(new String[mPermissionList.size()]), REQUEST_PERMISSION_REQ_CODE); + else checkForUngrantedPermissions(new String [] {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.GET_ACCOUNTS}); + } + + /** + * Method called when user has granted the coarse location permission to the application. + */ + protected abstract void onPermissionGranted(final String permission); + + /** + * Ensures the required permission has been granted by the user. On Android pre-6.0 it always returns true. + * In case the permission has not been granted this method may also show a dialog with rationale. + * @return true if the permission required for BLE scanning is granted + */ + + protected boolean ensurePermission(final String [] permissions) { + // Since Android 6.0 we need to obtain either Manifest.permission.ACCESS_COARSE_LOCATION or Manifest.permission.ACCESS_FINE_LOCATION to be able to scan for + // Bluetooth LE devices. This is related to beacons as proximity devices. + // On API older than Marshmallow the following code does nothing. + mPermissionList = new ArrayList<>(); + if(permissions.length > 0 ) { + for (int i = 0; i < permissions.length; i++) { + if (ContextCompat.checkSelfPermission(getContext(), permissions[i]) != PackageManager.PERMISSION_GRANTED) { + // When user pressed Deny and still wants to use this functionality, show the rationale + if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permissions[i])) { + if(permissions[i] == Manifest.permission.ACCESS_COARSE_LOCATION) { + final PermissionRationaleDialogFragment dialog = PermissionRationaleDialogFragment.getInstance(getString(R.string.rationale_message_location)); + dialog.show(getChildFragmentManager(), null); + return false; + } else if(permissions[i] == Manifest.permission.GET_ACCOUNTS) { + final PermissionRationaleDialogFragment dialog = PermissionRationaleDialogFragment.getInstance(getString(R.string.rationale_message_contacts)); + dialog.show(getChildFragmentManager(), null); + return false; + } + } else { + mPermissionList.add(permissions[i]); + } + } else { + onPermissionGranted(permissions[i]); + } + } + + if(mPermissionList.size() > 0) { + onRequestPermission(); + return true; + } + } + return true; + } + + protected void checkForUngrantedPermissions(final String [] permissions) { + mPermissionList = new ArrayList<>(); + if(permissions.length > 0 ) { + for (int i = 0; i < permissions.length; i++) { + if (ContextCompat.checkSelfPermission(getContext(), permissions[i]) != PackageManager.PERMISSION_GRANTED) { + // When user pressed Deny and still wants to use this functionality, show the rationale + mPermissionList.add(permissions[i]); + } + } + + if(mPermissionList.size() > 0) { + onRequestPermission(); + } + } + } + + + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/BoardHelpFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/BoardHelpFragment.java new file mode 100644 index 0000000..c065186 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/BoardHelpFragment.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.common; + +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +public class BoardHelpFragment extends DialogFragment { + public static final int MODE_DFU = 1; + public static final int MODE_UPDATE = 2; + + private static final String MODE = "mode"; + + public static BoardHelpFragment getInstance(final int mode) { + final BoardHelpFragment fragment = new BoardHelpFragment(); + + final Bundle args = new Bundle(); + args.putInt(MODE, mode); + fragment.setArguments(args); + + return fragment; + } + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + final Bundle bundle = getArguments(); + final int mode = bundle.getInt(MODE); + + getDialog().setTitle(R.string.update_about_title); + final TextView aboutView = (TextView) inflater.inflate(R.layout.fragment_dialog_help, container, false); + switch (mode) { + case MODE_DFU: + aboutView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.beacon_sw1, 0, 0, 0); + aboutView.setText(R.string.update_about_message_dfu); + break; + case MODE_UPDATE: + default: + aboutView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.beacon_sw2, 0, 0, 0); + aboutView.setText(R.string.update_about_message_update); + break; + } + return aboutView; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/PermissionRationaleDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/PermissionRationaleDialogFragment.java new file mode 100644 index 0000000..26f3307 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/common/PermissionRationaleDialogFragment.java @@ -0,0 +1,81 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + *

+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *

+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + *

+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + *

+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ + +package no.nordicsemi.android.nrfbeacon.nearby.common; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v7.app.AlertDialog; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + + +public class PermissionRationaleDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { + + private static final String RATIONALE_MESSAGE = "RATIONALE_MESSAGE"; + private String mRationaleMessage; + + public static PermissionRationaleDialogFragment getInstance(final String message){ + PermissionRationaleDialogFragment fragment = new PermissionRationaleDialogFragment(); + Bundle bundle = new Bundle(); + bundle.putString(RATIONALE_MESSAGE, message); + fragment.setArguments(bundle); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null) + mRationaleMessage = getArguments().getString(RATIONALE_MESSAGE); + } + + public interface PermissionDialogListener { + void onRequestPermission(); + } + + @Override + public void onAttach(Context activity) { + super.onAttach(activity); + } + + @NonNull + @Override + public Dialog onCreateDialog(final Bundle savedInstanceState) { + return new AlertDialog.Builder(getContext()).setTitle(R.string.rationale_title) + .setMessage(mRationaleMessage) + .setPositiveButton(R.string.rationale_request, this) + .setNegativeButton(R.string.rationale_cancel, null).create(); + } + + @Override + public void onClick(final DialogInterface dialogInterface, final int i) { + ((PermissionDialogListener)getParentFragment()).onRequestPermission(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/DeviceListAdapter.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/DeviceListAdapter.java new file mode 100644 index 0000000..d660967 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/DeviceListAdapter.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.scanner; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.support.v18.scanner.ScanResult; + +/** + * DeviceListAdapter class is list adapter for showing scanned Devices name, address and RSSI image based on RSSI values. + */ +public class DeviceListAdapter extends BaseAdapter { + private static final int TYPE_TITLE = 0; + private static final int TYPE_ITEM = 1; + private static final int TYPE_EMPTY = 2; + + private final List mDevices = new ArrayList<>(); + + /** + * If such device exists on the bonded device list, this method does nothing. If not then the device is updated (rssi value) or added. + * + * @param results scan results + */ + public void update(final List results) { + for (final ScanResult result : results) { + final ExtendedBluetoothDevice device = findDevice(result); + if (device == null) { + mDevices.add(new ExtendedBluetoothDevice(result)); + } else { + device.name = result.getScanRecord() != null ? result.getScanRecord().getDeviceName() : null; + device.rssi = result.getRssi(); + } + } + notifyDataSetChanged(); + } + + private ExtendedBluetoothDevice findDevice(final ScanResult result) { + for (final ExtendedBluetoothDevice device : mDevices) + if (device.matches(result)) + return device; + return null; + } + + public void clearDevices() { + if (mDevices != null) { + mDevices.clear(); + notifyDataSetChanged(); + } + } + + @Override + public int getCount() { + return mDevices.isEmpty() ? 2 : mDevices.size() + 1; // 1 for title, 1 for empty text + } + + @Override + public Object getItem(int position) { + if (position == 0) + return R.string.scanner_subtitle__not_bonded; + else + return mDevices.get(position - 1); + } + + @Override + public int getViewTypeCount() { + return 3; + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return getItemViewType(position) == TYPE_ITEM; + } + + @Override + public int getItemViewType(int position) { + if (position == 0) + return TYPE_TITLE; + + if (position == getCount() - 1 && mDevices.isEmpty()) + return TYPE_EMPTY; + + return TYPE_ITEM; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View oldView, ViewGroup parent) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + final int type = getItemViewType(position); + + View view = oldView; + switch (type) { + case TYPE_EMPTY: + if (view == null) { + view = inflater.inflate(R.layout.device_list_empty, parent, false); + } + break; + case TYPE_TITLE: + if (view == null) { + view = inflater.inflate(R.layout.device_list_title, parent, false); + } + final TextView title = (TextView) view; + title.setText((Integer) getItem(position)); + break; + default: + if (view == null) { + view = inflater.inflate(R.layout.device_list_row, parent, false); + final ViewHolder holder = new ViewHolder(); + holder.name = (TextView) view.findViewById(R.id.name); + holder.address = (TextView) view.findViewById(R.id.address); + holder.rssi = (ImageView) view.findViewById(R.id.rssi); + view.setTag(holder); + } + + final ExtendedBluetoothDevice device = (ExtendedBluetoothDevice) getItem(position); + final ViewHolder holder = (ViewHolder) view.getTag(); + final String name = device.name; + holder.name.setText(name != null ? name : parent.getContext().getString(R.string.not_available)); + holder.address.setText(device.device.getAddress()); + if (device.rssi != ScannerFragment.NO_RSSI) { + final int rssiPercent = (int) (100.0f * (127.0f + device.rssi) / (127.0f + 20.0f)); + holder.rssi.setImageLevel(rssiPercent); + holder.rssi.setVisibility(View.VISIBLE); + } else { + holder.rssi.setVisibility(View.GONE); + } + break; + } + + return view; + } + + private class ViewHolder { + private TextView name; + private TextView address; + private ImageView rssi; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ExtendedBluetoothDevice.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ExtendedBluetoothDevice.java new file mode 100644 index 0000000..dd7445e --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ExtendedBluetoothDevice.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.scanner; + +import android.bluetooth.BluetoothDevice; + +import no.nordicsemi.android.support.v18.scanner.ScanResult; + +public class ExtendedBluetoothDevice { + public BluetoothDevice device; + public int rssi; + public String name; + + public ExtendedBluetoothDevice(final ScanResult scanResult) { + this.device = scanResult.getDevice(); + this.name = scanResult.getScanRecord() != null ? scanResult.getScanRecord().getDeviceName() : null; + this.rssi = scanResult.getRssi(); + } + + public boolean matches(final ScanResult scanResult) { + return device.getAddress().equals(scanResult.getDevice().getAddress()); + } + + @Override + public boolean equals(Object o) { + if (o instanceof ExtendedBluetoothDevice) { + final ExtendedBluetoothDevice that = (ExtendedBluetoothDevice) o; + return device.getAddress().equals(that.device.getAddress()); + } + return super.equals(o); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ScannerFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ScannerFragment.java new file mode 100644 index 0000000..c38798b --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ScannerFragment.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.scanner; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat; +import no.nordicsemi.android.support.v18.scanner.ScanCallback; +import no.nordicsemi.android.support.v18.scanner.ScanFilter; +import no.nordicsemi.android.support.v18.scanner.ScanResult; +import no.nordicsemi.android.support.v18.scanner.ScanSettings; + +import android.Manifest; +import android.app.Dialog; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.ParcelUuid; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.DialogFragment; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.ListView; +import android.widget.Toast; + +/** + * ScannerFragment class scan required BLE devices and shows them in a list. This class scans and filter devices with given BLE Service UUID which may be null. It contains a + * list and a button to scan/cancel. The scanning will continue for 5 seconds and then stop. + */ +public class ScannerFragment extends DialogFragment { + private final static String TAG = "ScannerFragment"; + + private final static String PARAM_UUID = "param_uuid"; + private final static long SCAN_DURATION = 8000; + /* package */static final int NO_RSSI = -1000; + + private final static int REQUEST_PERMISSION_REQ_CODE = 76; // any 8-bit number + + private DeviceListAdapter mAdapter; + private Handler mHandler = new Handler(); + private Button mScanButton; + private View mPermissionRationale; + + private ParcelUuid mUuid; + private boolean mIsScanning = false; + + /** + * Static implementation of fragment so that it keeps data when phone orientation is changed For standard BLE Service UUID, we can filter devices using normal android provided command + * startScanLe() with required BLE Service UUID For custom BLE Service UUID, we will use class ScannerServiceParser to filter out required device + */ + public static ScannerFragment getInstance(final UUID uuid) { + final ScannerFragment fragment = new ScannerFragment(); + + final Bundle args = new Bundle(); + if (uuid != null) + args.putParcelable(PARAM_UUID, new ParcelUuid(uuid)); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Bundle args = getArguments(); + mUuid = args.getParcelable(PARAM_UUID); + } + + @Override + public void onDestroyView() { + stopScan(); + super.onDestroyView(); + } + + /** + * When dialog is created then set AlertDialog with list and button views + */ + @NonNull + @Override + public Dialog onCreateDialog(final Bundle savedInstanceState) { + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final View dialogView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_scanner_device_selection, null); + final ListView listview = (ListView) dialogView.findViewById(android.R.id.list); + + listview.setEmptyView(dialogView.findViewById(android.R.id.empty)); + listview.setAdapter(mAdapter = new DeviceListAdapter()); + + builder.setTitle(R.string.scanner_title); + final AlertDialog dialog = builder.setView(dialogView).create(); + listview.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(final AdapterView parent, final View view, final int position, final long id) { + stopScan(); + dismiss(); + + final ScannerFragmentListener listener = (ScannerFragmentListener) getParentFragment(); + final ExtendedBluetoothDevice device = (ExtendedBluetoothDevice) mAdapter.getItem(position); + listener.onDeviceSelected(device.device, device.name != null ? device.name : getString(R.string.not_available)); + } + }); + + mPermissionRationale = dialogView.findViewById(R.id.permission_rationale); // this is not null only on API23+ + + mScanButton = (Button) dialogView.findViewById(R.id.action_cancel); + mScanButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getId() == R.id.action_cancel) { + if (mIsScanning) { + dialog.cancel(); + } else { + startScan(); + } + } + } + }); + + if (savedInstanceState == null) + startScan(); + return dialog; + } + + @Override + public void onRequestPermissionsResult(final int requestCode, final @NonNull String[] permissions, final @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_PERMISSION_REQ_CODE: { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // We have been granted the Manifest.permission.ACCESS_COARSE_LOCATION permission. Now we may proceed with scanning. + startScan(); + } else { + mPermissionRationale.setVisibility(View.VISIBLE); + Toast.makeText(getActivity(), R.string.rationale_permission_denied, Toast.LENGTH_SHORT).show(); + } + break; + } + } + } + + /** + * Scan for 5 seconds and then stop scanning when a BluetoothLE device is found then mLEScanCallback is activated This will perform regular scan for custom BLE Service UUID and then filter out + * using class ScannerServiceParser + */ + private void startScan() { + // Since Android 6.0 we need to obtain either Manifest.permission.ACCESS_COARSE_LOCATION or Manifest.permission.ACCESS_FINE_LOCATION to be able to scan for + // Bluetooth LE devices. This is related to beacons as proximity devices. + // On API older than Marshmallow the following code does nothing. + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // When user pressed Deny and still wants to use this functionality, show the rationale + if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.ACCESS_COARSE_LOCATION) && mPermissionRationale.getVisibility() == View.GONE) { + mPermissionRationale.setVisibility(View.VISIBLE); + return; + } + + requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_PERMISSION_REQ_CODE); + return; + } + + // Hide the rationale message, we don't need it anymore. + if (mPermissionRationale != null) + mPermissionRationale.setVisibility(View.GONE); + + mAdapter.clearDevices(); + mScanButton.setText(R.string.scanner_action_cancel); + + final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); + final ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).setReportDelay(1000).setUseHardwareBatchingIfSupported(false).setUseHardwareFilteringIfSupported(false).build(); + final List filters = new ArrayList<>(); + filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build()); + scanner.startScan(filters, settings, scanCallback); + + mIsScanning = true; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (mIsScanning) { + stopScan(); + } + } + }, SCAN_DURATION); + } + + /** + * Stop scan if user tap Cancel button + */ + private void stopScan() { + if (mIsScanning) { + mScanButton.setText(R.string.scanner_action_scan); + + final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); + scanner.stopScan(scanCallback); + + mIsScanning = false; + } + } + + private ScanCallback scanCallback = new ScanCallback() { + @Override + public void onScanResult(final int callbackType, final ScanResult result) { + // do nothing + } + + @Override + public void onBatchScanResults(final List results) { + mAdapter.update(results); + } + + @Override + public void onScanFailed(final int errorCode) { + // should never be called + } + }; +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ScannerFragmentListener.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ScannerFragmentListener.java new file mode 100644 index 0000000..e0290bf --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/scanner/ScannerFragmentListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.scanner; + +import android.bluetooth.BluetoothDevice; + +public interface ScannerFragmentListener { + + /** + * Called when user has selected the device. + * + * @param device the selected device. May not be null. + * @param name the device name. + */ + public void onDeviceSelected(final BluetoothDevice device, final String name); +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/settings/NearbySettingsActivity.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/settings/NearbySettingsActivity.java new file mode 100644 index 0000000..95a7e84 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/settings/NearbySettingsActivity.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.settings; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.MenuItem; + +import no.nordicsemi.android.nrfbeacon.nearby.MainActivity; +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.beacon.BeaconsFragment; + +public class NearbySettingsActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener{ + + private boolean mBackgroundScanningEnabled; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_nearby_settings); + final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setTitle(getString(R.string.settings_title)); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + mBackgroundScanningEnabled = PreferenceManager.getDefaultSharedPreferences(NearbySettingsActivity.this).getBoolean(getString(R.string.nearby_settings_key), false); + if(savedInstanceState == null) + getFragmentManager().beginTransaction().replace(R.id.content, new NearbySettingsFragment()).commit(); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()){ + case android.R.id.home: + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + boolean flag = PreferenceManager.getDefaultSharedPreferences(NearbySettingsActivity.this).getBoolean(getString(R.string.nearby_settings_key), false); + if(mBackgroundScanningEnabled != flag) { + mBackgroundScanningEnabled = flag; + Intent result = new Intent(); + result.setData(Uri.parse(String.valueOf(mBackgroundScanningEnabled))); + setResult(RESULT_OK, result); + } + super.onBackPressed(); + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/settings/NearbySettingsFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/settings/NearbySettingsFragment.java new file mode 100644 index 0000000..277ebe1 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/settings/NearbySettingsFragment.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.settings; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceFragment; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +public class NearbySettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + + private boolean mBackgroundScanningEnabled; + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.settings_nearby); + } + @Override + public void onResume() { + super.onResume(); + + // attach the preference change listener. It will update the summary below interval preference + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + // unregister listener + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { + final SharedPreferences preferences = getPreferenceManager().getSharedPreferences(); + mBackgroundScanningEnabled = preferences.getBoolean(key, false); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/AdvertisingIntervalDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/AdvertisingIntervalDialogFragment.java new file mode 100644 index 0000000..e9a99e5 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/AdvertisingIntervalDialogFragment.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +/** + * Created by rora on 06.04.2016. + */ +public class AdvertisingIntervalDialogFragment extends DialogFragment { + private static final String ADV_INTERVAL = "ADV_INTERVAL"; + private static final String TAG = "BEACON"; + + private String mCurrentAdvInterval; + private EditText advInterval; + + public interface OnAdvertisingIntervalListener { + void configureAdvertisingInterval(final byte [] advertisingInterval); + } + + public static AdvertisingIntervalDialogFragment newInstance(final String currentAdvInterval){ + AdvertisingIntervalDialogFragment fragment = new AdvertisingIntervalDialogFragment(); + final Bundle args = new Bundle(); + args.putString(ADV_INTERVAL, currentAdvInterval); + fragment.setArguments(args); + return fragment; + } + + public AdvertisingIntervalDialogFragment(){} + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mCurrentAdvInterval = getArguments().getString(ADV_INTERVAL); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.title_config_radio_tx_power)); + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_dialog_advertising_interval, null); + final TextView textView = (TextView) view.findViewById(R.id.current_adv_interval); + textView.setText(mCurrentAdvInterval); + advInterval = (EditText) view.findViewById(R.id.advertising_interval); + final AlertDialog alertDialog = alertDialogBuilder.setView(view).setPositiveButton(getString(R.string.configure), null).setNegativeButton(getString(R.string.cancel), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(validateInput()){ + final byte [] advertisingInterval = getValueFromView(); + ((OnAdvertisingIntervalListener)getParentFragment()).configureAdvertisingInterval(advertisingInterval); + dismiss(); + } + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return alertDialog; + } + + private boolean validateInput() { + final String advertisingInterval = advInterval.getText().toString().trim(); + + if(advertisingInterval.isEmpty()){ + advInterval.setError("Enter advertising interval value"); + return false; + } else { + boolean valid; + try { + int i = Integer.parseInt(advertisingInterval); + valid = (i & 0xFFFF0000) == 0 || (i & 0xFFFF0000) == 0xFFFF0000; + } catch (NumberFormatException e) { + valid = false; + } + if (!valid) { + advInterval.setError("Value does not match UINT16"); + return false; + } + } + + return true; + } + + private byte[] getValueFromView() { + final byte [] data = new byte[2]; + final int value = Integer.parseInt(advInterval.getText().toString().trim()); + ParserUtils.setValue(data, 0, value, ParserUtils.FORMAT_UINT16_BIG_INDIAN); + return data; + } + + @Override + public void onDetach() { + super.onDetach(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/AllSlotInfoDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/AllSlotInfoDialogFragment.java new file mode 100644 index 0000000..7c73f74 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/AllSlotInfoDialogFragment.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.LinearLayoutCompat; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +/** + * Created by rora on 07.03.2016. + */ +public class AllSlotInfoDialogFragment extends DialogFragment { + + private static final String ALL_SLOT_INFO = "ALL_SLOT_INFO"; + private ArrayList mAllSlotInfo; + + public static AllSlotInfoDialogFragment newInstance(final ArrayList allSlotInfo){ + AllSlotInfoDialogFragment fragment = new AllSlotInfoDialogFragment(); + final Bundle args = new Bundle(); + args.putStringArrayList(ALL_SLOT_INFO, allSlotInfo); + fragment.setArguments(args); + return fragment; + } + + public AllSlotInfoDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mAllSlotInfo = getArguments().getStringArrayList(ALL_SLOT_INFO); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle("All Slot Information"); + alertDialogBuilder.setMessage("Following slots have been configured in the beacon"); + + final View alertDialogView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_all_slot_info, null); + final LinearLayout slotTitleContainer = (LinearLayout) alertDialogView.findViewById(R.id.slot_title_container); + final LinearLayout slotInfoContainer = (LinearLayout) alertDialogView.findViewById(R.id.slot_info_container); + LinearLayout.LayoutParams lParams = new LinearLayout.LayoutParams(LinearLayoutCompat.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + for(int i = 0; i < mAllSlotInfo.size(); i++) { + TextView slotTitle = new TextView(getActivity()); + slotTitle.setLayoutParams(lParams); + slotTitle.setText("Slot " + i); + slotTitleContainer.addView(slotTitle); + + TextView slotInfo = new TextView(getActivity()); + slotInfo.setLayoutParams(lParams); + slotInfo.setText(mAllSlotInfo.get(i)); + slotInfoContainer.addView(slotInfo); + + if(i > 0){ + lParams.setMargins(0, 10, 0, 0); + } + + } + + final AlertDialog alertDialog = alertDialogBuilder.setView(alertDialogView).setPositiveButton(getString(R.string.ok), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + + + return alertDialog; + } + + @Override + public void onDetach() { + super.onDetach(); + } + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/BroadcastCapabilitesDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/BroadcastCapabilitesDialogFragment.java new file mode 100644 index 0000000..f23cb78 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/BroadcastCapabilitesDialogFragment.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +/** + * Created by rora on 07.03.2016. + */ +public class BroadcastCapabilitesDialogFragment extends DialogFragment { + + private static final String PATTERN_TX_POWER = "[0-9a-fA-F]{32}"; + private static final String BROADCAST_CAPABILITIES = "BROADCAST_CAPABILITIES"; + private static final String TAG = "BEACON"; + private static final int IS_VARIABLE_ADV_SUPPORTED = 0x01; + private static final int IS_VARIABLE_TX_POWER_SUPPORTED = 0x02; + private static final int TYPE_UID = 0x0001; + private static final int TYPE_URL = 0x0002; + private static final int TYPE_TLM = 0x0004; + private static final int TYPE_EID = 0x0008; + private TextView mVersion; + + private byte [] mBroadcastCapabilities; + + public static BroadcastCapabilitesDialogFragment newInstance(byte [] broadcastCapabilities){ + BroadcastCapabilitesDialogFragment fragment = new BroadcastCapabilitesDialogFragment(); + final Bundle args = new Bundle(); + args.putByteArray(BROADCAST_CAPABILITIES, broadcastCapabilities); + fragment.setArguments(args); + return fragment; + } + + public BroadcastCapabilitesDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mBroadcastCapabilities = getArguments().getByteArray(BROADCAST_CAPABILITIES); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle("Broadcast Capabilities"); + + final View alertDialogView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_broadcast_capabilities, null); + mVersion = (TextView) alertDialogView.findViewById(R.id.version_value); + final TextView totalSlots = (TextView) alertDialogView.findViewById(R.id.total_slots); + final TextView totalEidSlots = (TextView) alertDialogView.findViewById(R.id.total_eid_slots); + final TextView variableAdvertisingSupported = (TextView) alertDialogView.findViewById(R.id.variable_adv_supported); + final TextView variableTxPowerSupported = (TextView) alertDialogView.findViewById(R.id.variable_tx_power_supported); + final TextView supportedFrameTypes = (TextView) alertDialogView.findViewById(R.id.frame_types); + final TextView supportedTxPower = (TextView) alertDialogView.findViewById(R.id.supported_tx_power); + + final AlertDialog alertDialog = alertDialogBuilder.setView(alertDialogView).setPositiveButton(getString(R.string.ok), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + mVersion.setText(String.valueOf(ParserUtils.getIntValue(mBroadcastCapabilities, 0, BluetoothGattCharacteristic.FORMAT_UINT8))); + totalSlots.setText(String.valueOf(ParserUtils.getIntValue(mBroadcastCapabilities, 1, BluetoothGattCharacteristic.FORMAT_UINT8))); + totalEidSlots.setText(String.valueOf(ParserUtils.getIntValue(mBroadcastCapabilities, 2, BluetoothGattCharacteristic.FORMAT_UINT8))); + + + final int capabilities = ParserUtils.getIntValue(mBroadcastCapabilities, 3, BluetoothGattCharacteristic.FORMAT_UINT8); + final boolean variableAdvertising = (capabilities & IS_VARIABLE_ADV_SUPPORTED) > 0; + if(variableAdvertising) + variableAdvertisingSupported.setText("YES"); + else + variableAdvertisingSupported.setText("NO"); + + final boolean variableTxPower = (capabilities & IS_VARIABLE_TX_POWER_SUPPORTED) > 0; + if(variableTxPower) + variableTxPowerSupported.setText("YES"); + else + variableTxPowerSupported.setText("NO"); + + final int supportedEddystoneFrameTypes = ParserUtils.getIntValue(mBroadcastCapabilities, 4, ParserUtils.FORMAT_UINT16_BIG_INDIAN); + + final boolean typeUid = (supportedEddystoneFrameTypes & TYPE_UID) > 0; + final boolean typeUrl = (supportedEddystoneFrameTypes & TYPE_URL) > 0; + final boolean typeTlm = (supportedEddystoneFrameTypes & TYPE_TLM) > 0; + final boolean typeEid = (supportedEddystoneFrameTypes & TYPE_EID) > 0; + + StringBuilder builder = new StringBuilder(); + + if(typeUid) + builder.append("UID, "); + if(typeUrl) + builder.append("URL, "); + if(typeTlm) + builder.append("TLM, "); + if(typeEid) + builder.append("EID"); + + if(builder.toString().endsWith(",")) { + builder.setLength(builder.length() - 2); + } + + supportedFrameTypes.setText(builder.toString()); + + builder = new StringBuilder(); + for(int i = 6; i < mBroadcastCapabilities.length; i++){ + builder.append(" " + ParserUtils.getIntValue(mBroadcastCapabilities, i , BluetoothGattCharacteristic.FORMAT_SINT8)).append(","); + } + + if(builder.toString().endsWith(",")){ + builder.setLength(builder.length()-1); + } + builder.append(" dBm"); + supportedTxPower.setText(builder.toString()); + + return alertDialog; + } + + @Override + public void onDetach() { + super.onDetach(); + } + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ClearSlotDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ClearSlotDialogFragment.java new file mode 100644 index 0000000..bd416bf --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ClearSlotDialogFragment.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +/** + * Created by rora on 16.03.2016. + */ +public class ClearSlotDialogFragment extends DialogFragment{ + + private static final String MESSAGE = "MESSAGE"; + private String mMessage; + + public interface OnClearSlotListener { + void clearSlot(); + } + + public static ClearSlotDialogFragment newInstance(){ + ClearSlotDialogFragment fragment = new ClearSlotDialogFragment(); + return fragment; + } + + public ClearSlotDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.action_clear_slot)); + alertDialogBuilder.setMessage(getString(R.string.action_clear_slot_message)); + final AlertDialog alertDialog = alertDialogBuilder.setPositiveButton(getString(R.string.yes), null).setNegativeButton(getString(R.string.no), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ((OnClearSlotListener) getParentFragment()).clearSlot(); + dismiss(); + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return alertDialog; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/CreateAttachmentDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/CreateAttachmentDialogFragment.java new file mode 100644 index 0000000..7327a9f --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/CreateAttachmentDialogFragment.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; + +import java.nio.charset.Charset; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +/** + * Created by rora on 07.03.2016. + */ +public class CreateAttachmentDialogFragment extends DialogFragment { + + private static final String TAG = "BEACON"; + private static final String BEACON_NAME = "BEACON_NAME"; + private String mBeaconName; + private EditText mAttachment; + + public interface OnAttachmentCreatedListener { + void createAttachmentForBeacon(final String mBeaconName, final byte[] attachmentData); + } + + public static CreateAttachmentDialogFragment newInstance(final String beaconName){ + CreateAttachmentDialogFragment fragment = new CreateAttachmentDialogFragment(); + Bundle args = new Bundle(); + args.putString(BEACON_NAME, beaconName); + fragment.setArguments(args); + return fragment; + } + + public CreateAttachmentDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null) + mBeaconName = getArguments().getString(BEACON_NAME); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.create_attachment_title)); + alertDialogBuilder.setMessage(getString(R.string.create_attachment_message)); + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_create_beacon_attachment, null); + mAttachment = (EditText) view.findViewById(R.id.attachment); + + final AlertDialog alertDialog = alertDialogBuilder.setView(view).setPositiveButton(getString(R.string.create), null).setNegativeButton(getString(R.string.cancel), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(validateInput()) { + final byte [] attachmentData = mAttachment.getText().toString().trim().getBytes(Charset.forName("UTF-8")); + ((OnAttachmentCreatedListener)getParentFragment()).createAttachmentForBeacon(mBeaconName, attachmentData); + dismiss(); + } + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + return alertDialog; + } + + @Override + public void onDetach() { + super.onDetach(); + } + + private boolean validateInput(){ + final String attachment = mAttachment.getText().toString().trim(); + if(attachment.isEmpty()){ + mAttachment.setError(getString(R.string.enter_attachment_data)); + return false; + } + return true; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/EcdhKeyInfoDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/EcdhKeyInfoDialogFragment.java new file mode 100644 index 0000000..c6f5a81 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/EcdhKeyInfoDialogFragment.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +/** + * Created by rora on 16.03.2016. + */ +public class EcdhKeyInfoDialogFragment extends DialogFragment { + + + private static final String BEACON_ECDH_PUBLIC_KEY = "BEACON_ECDH_PUBLIC_KEY"; + private static final String ENCRYPTED_IDENTITY_KEY = "ENCRYPTED_IDENTITY_KEY"; + private static final String DECRYPTED_IDENTITY_KEY = "DECRYPTED_IDENTITY_KEY"; + private static final String FRAME_TYPE = "FRAME_TYPE"; + private static final String ACTIVE_SLOT = "ACTIVE_SLOT"; + + private static final int TYPE_UID = 0x00; + private static final int TYPE_URL = 0x10; + private static final int TYPE_TLM = 0x20; + private static final int TYPE_EID = 0x30; + private int mFrameType; + private int mActiveSlot; + private String mEncryptedIdentityKey; + private String mDecryptedIdentityKey; + private String mPublicEcdhKey; + + public static EcdhKeyInfoDialogFragment newInstance(final int activeSlot, final int frameType, final String beaconEcdhKeyPublicKey, final String encryptedIdentityKey, final String decryptedIdentityKey){ + EcdhKeyInfoDialogFragment fragment = new EcdhKeyInfoDialogFragment(); + Bundle args = new Bundle(); + args.putInt(ACTIVE_SLOT, activeSlot); + args.putInt(FRAME_TYPE, frameType); + args.putString(BEACON_ECDH_PUBLIC_KEY, beaconEcdhKeyPublicKey); + args.putString(ENCRYPTED_IDENTITY_KEY, encryptedIdentityKey); + args.putString(DECRYPTED_IDENTITY_KEY, decryptedIdentityKey); + fragment.setArguments(args); + return fragment; + } + + public EcdhKeyInfoDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + if(getArguments() != null){ + mActiveSlot = getArguments().getInt(ACTIVE_SLOT); + mFrameType = getArguments().getInt(FRAME_TYPE); + mPublicEcdhKey = getArguments().getString(BEACON_ECDH_PUBLIC_KEY); + mEncryptedIdentityKey = getArguments().getString(ENCRYPTED_IDENTITY_KEY); + mDecryptedIdentityKey = getArguments().getString(DECRYPTED_IDENTITY_KEY); + } + super.onCreate(savedInstanceState); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.title_ecdh_key_info)); + + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_dialog_ecdh, null); + AlertDialog alertDialog = alertDialogBuilder.setView(view).setPositiveButton(getString(R.string.ok), null).show(); + final TextView activeSlot = (TextView) view.findViewById(R.id.active_slot); + final TextView frameType = (TextView) view.findViewById(R.id.frame_type); + final TextView ecdhKey = (TextView) view.findViewById(R.id.ecdh_public_key); + final TextView encryptedIdentityKey = (TextView) view.findViewById(R.id.encrypted_ecdh_identity_key); + final TextView decryptedIdentityKey = (TextView) view.findViewById(R.id.decrypted_ecdh_identity_key); + activeSlot.setText(String.valueOf(mActiveSlot)); + ecdhKey.setText(mPublicEcdhKey); + + //display identity key only if the active slot frame type is EID as the identity keys will differ for each slot + switch (mFrameType){ + case TYPE_UID: + frameType.setText(getString(R.string.type_uid)); + encryptedIdentityKey.setText(getString(R.string.no_identity_key)); + decryptedIdentityKey.setText(getString(R.string.no_identity_key)); + break; + case TYPE_URL: + frameType.setText(getString(R.string.type_url)); + encryptedIdentityKey.setText(getString(R.string.no_identity_key)); + decryptedIdentityKey.setText(getString(R.string.no_identity_key)); + break; + case TYPE_TLM: + frameType.setText(getString(R.string.type_tlm)); + encryptedIdentityKey.setText(getString(R.string.no_identity_key)); + decryptedIdentityKey.setText(getString(R.string.no_identity_key)); + break; + case TYPE_EID: + frameType.setText(getString(R.string.type_tlm)); + encryptedIdentityKey.setText(mEncryptedIdentityKey); + decryptedIdentityKey.setText(mDecryptedIdentityKey); + break; + } + + + + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return alertDialog; + + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ErrorDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ErrorDialogFragment.java new file mode 100644 index 0000000..793442e --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ErrorDialogFragment.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +/** + * Created by rora on 16.03.2016. + */ +public class ErrorDialogFragment extends DialogFragment{ + + private static final String MESSAGE = "MESSAGE"; + private String mMessage; + + + public static ErrorDialogFragment newInstance(final String message){ + ErrorDialogFragment fragment = new ErrorDialogFragment(); + final Bundle args = new Bundle(); + args.putString(MESSAGE, message); + fragment.setArguments(args); + return fragment; + } + + public ErrorDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mMessage = getArguments().getString(MESSAGE); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.error_generic)); + alertDialogBuilder.setMessage(mMessage); + final AlertDialog alertDialog = alertDialogBuilder.setPositiveButton(getString(R.string.ok), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return alertDialog; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/LockStateDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/LockStateDialogFragment.java new file mode 100644 index 0000000..d0251e9 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/LockStateDialogFragment.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.Arrays; + +import javax.crypto.spec.SecretKeySpec; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +/** + * Created by rora on 07.03.2016. + */ +public class LockStateDialogFragment extends DialogFragment { + + private static final String PATTERN_LOCK_STATE = "[0-9a-fA-F]{32}"; + private static final String LOCK_STATE = "LOCK_STATE"; + private static final String UNLOCK_CODE = "UNLOCK_CODE"; + private static final String TAG = "BEACON"; + private static final int LOCKED = 0x00; + private static final int UNLOCKED = 0x01; + private static final int UNLOCKED_AUTOMATIC_RELOCK_DISABLED = 0x02; + + private int mCurrentLockState; + private byte [] mUnlockCode; + private EditText mOldLockCode; + private EditText mNewLockCode; + private TextView mTitleOldLockCode; + private TextView mTitleNewLockCode; + private LinearLayout mOldLockCodeContainer; + private LinearLayout mNewLockCodeContainer; + private Spinner mLockStates; + + public interface OnLockStateListener { + void lockBeacon(byte[] lockCode); + } + + public static LockStateDialogFragment newInstance(final int currentLockState, final byte [] unlockCode){ + LockStateDialogFragment fragment = new LockStateDialogFragment(); + final Bundle args = new Bundle(); + args.putInt(LOCK_STATE, currentLockState); + args.putByteArray(UNLOCK_CODE, unlockCode); + fragment.setArguments(args); + return fragment; + } + + public LockStateDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mCurrentLockState = getArguments().getInt(LOCK_STATE); + mUnlockCode = getArguments().getByteArray(UNLOCK_CODE); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle("Lock State"); + + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_dialog_lock_state, null); + mLockStates = (Spinner) view.findViewById(R.id.frame_types); + mOldLockCode = (EditText) view.findViewById(R.id.old_lock_code); + mNewLockCode = (EditText) view.findViewById(R.id.new_lock_code); + mTitleOldLockCode = (TextView) view.findViewById(R.id.title_old_lock_code); + mTitleNewLockCode = (TextView) view.findViewById(R.id.title_new_lock_code); + mOldLockCodeContainer = (LinearLayout) view.findViewById(R.id.old_lock_code_container); + mNewLockCodeContainer = (LinearLayout) view.findViewById(R.id.new_lock_code_container); + + mLockStates.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + switch (position){ + case 0: + mTitleOldLockCode.setVisibility(View.GONE); + mOldLockCodeContainer.setVisibility(View.GONE); + mTitleNewLockCode.setVisibility(View.GONE); + mNewLockCodeContainer.setVisibility(View.GONE); + break; + case 1: + mTitleOldLockCode.setVisibility(View.VISIBLE); + mOldLockCodeContainer.setVisibility(View.VISIBLE); + mTitleNewLockCode.setVisibility(View.VISIBLE); + mNewLockCodeContainer.setVisibility(View.VISIBLE); + break; + case 2: + mTitleOldLockCode.setVisibility(View.GONE); + mOldLockCodeContainer.setVisibility(View.GONE); + mTitleNewLockCode.setVisibility(View.GONE); + mNewLockCodeContainer.setVisibility(View.GONE); + break; + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + updateCurrentLockState(); + + final AlertDialog alertDialog = alertDialogBuilder.setView(view).setPositiveButton(getString(R.string.ok), null).setNegativeButton(getString(R.string.cancel), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(validateInput()){ + final byte [] lockCodeData = getValueFromView(); + ((OnLockStateListener)getParentFragment()).lockBeacon(lockCodeData); + dismiss(); + } + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return alertDialog; + } + + private void updateCurrentLockState() { + switch(mCurrentLockState){ + case LOCKED: + mLockStates.setSelection(0); + break; + case UNLOCKED: + mLockStates.setSelection(1); + break; + case UNLOCKED_AUTOMATIC_RELOCK_DISABLED: + mLockStates.setSelection(2); + break; + } + } + + private boolean validateInput() { + + if(mOldLockCodeContainer.getVisibility() == View.VISIBLE) { + final String oldLockCode = mOldLockCode.getText().toString().trim(); + if(oldLockCode.isEmpty()){ + mOldLockCode.setError("Please enter old lock code"); + return false; + } else if (!oldLockCode.matches(PATTERN_LOCK_STATE)) { + mOldLockCode.setError("Please enter a valid value for old lock code"); + return false; + } + final byte [] oldBeaconLockCode = new byte[16]; + ParserUtils.setByteArrayValue(oldBeaconLockCode, 0, oldLockCode); + if(!Arrays.equals(mUnlockCode, oldBeaconLockCode)){ + Toast.makeText(getActivity(), getString(R.string.lock_code_error), Toast.LENGTH_SHORT).show(); + return false; + } + + } + + if(mNewLockCodeContainer.getVisibility() == View.VISIBLE) { + final String newLockCode = mNewLockCode.getText().toString().trim(); + if (newLockCode.isEmpty()) { + mNewLockCode.setError("Please enter new lock code"); + return false; + } else { + if (!newLockCode.matches(PATTERN_LOCK_STATE)) { + mNewLockCode.setError("Please enter a valid value for new lock code"); + return false; + } + } + } + return true; + } + + private byte[] getValueFromView() { + + final int lockState = mLockStates.getSelectedItemPosition(); + final String oldLockCode = mOldLockCode.getText().toString(); + final String newLockCode = mNewLockCode.getText().toString(); + final byte [] data; + + if(lockState == 0 || lockState == 2){ + data = new byte[1]; + data[0] = (byte)lockState; + return data; + } else { + data = new byte[17]; //lockstate + security key + data[0] = (byte)(lockState-1); + + final byte [] oldLockCodeBytes = new byte [16]; + ParserUtils.setByteArrayValue(oldLockCodeBytes, 0, oldLockCode); + + final byte [] newLockCodeBytes = new byte[16]; + ParserUtils.setByteArrayValue(newLockCodeBytes, 0, newLockCode); + + final byte [] encryptedLockCode = ParserUtils.aes128Encrypt(newLockCodeBytes, new SecretKeySpec(oldLockCodeBytes, "AES")); + System.arraycopy(encryptedLockCode, 0, data, 1, encryptedLockCode.length); + return data; + } + } + + @Override + public void onDetach() { + super.onDetach(); + } + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ProximityApiErrorDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ProximityApiErrorDialogFragment.java new file mode 100644 index 0000000..32dae13 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ProximityApiErrorDialogFragment.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +/** + * Created by rora on 16.03.2016. + */ +public class ProximityApiErrorDialogFragment extends DialogFragment{ + + private static final String ERROR_CODE = "ERROR_CODE"; + private static final String MESSAGE = "MESSAGE"; + private static final String STATUS = "STATUS"; + private String mErrorCode; + private String mMessage; + private String mStatus; + + + public static ProximityApiErrorDialogFragment newInstance(final String errorCode, final String message, final String status){ + ProximityApiErrorDialogFragment fragment = new ProximityApiErrorDialogFragment(); + final Bundle args = new Bundle(); + args.putString(ERROR_CODE, errorCode); + args.putString(MESSAGE, message); + args.putString(STATUS, status); + fragment.setArguments(args); + return fragment; + } + + public ProximityApiErrorDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mErrorCode = getArguments().getString(ERROR_CODE); + mMessage = getArguments().getString(MESSAGE); + mStatus = getArguments().getString(STATUS); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle("Error " + mErrorCode); + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_dialog_error, null); + final TextView message = (TextView) view.findViewById(R.id.error_message); + final TextView status = (TextView) view.findViewById(R.id.error_status); + message.setText(mMessage); + status.setText(mStatus); + final AlertDialog alertDialog = alertDialogBuilder.setView(view).setPositiveButton(getString(R.string.ok), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return alertDialog; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RadioTxPowerDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RadioTxPowerDialogFragment.java new file mode 100644 index 0000000..b7c2fe3 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RadioTxPowerDialogFragment.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +/** + * Created by rora on 06.04.2016. + */ +public class RadioTxPowerDialogFragment extends DialogFragment { + + private static final String PATTERN_TX_POWER = "(-?\\d{1,3}(|$))+"; + private static final String RADIO_TX_POWER = "RADIO_TX_POWER"; + private static final String ADVANCED_ADV_TX_POWER = "ADVANCED_ADV_TX_POWER"; + private static final String TAG = "BEACON"; + + private String mRadioTxPower; + private boolean mAdvancedAdvTxPower; + private EditText radioTxPower; + + public interface OnRadioTxPowerListener { + void configureRadioTxPower(final byte [] radioTxPower, final boolean advanceTxPowerSupported); + } + + public static RadioTxPowerDialogFragment newInstance(final String radioTxPower, final boolean advancedAdvTxPower){ + RadioTxPowerDialogFragment fragment = new RadioTxPowerDialogFragment(); + final Bundle args = new Bundle(); + args.putString(RADIO_TX_POWER, radioTxPower); + args.putBoolean(ADVANCED_ADV_TX_POWER, advancedAdvTxPower); + fragment.setArguments(args); + return fragment; + } + + public RadioTxPowerDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mRadioTxPower = getArguments().getString(RADIO_TX_POWER); + mAdvancedAdvTxPower = getArguments().getBoolean(ADVANCED_ADV_TX_POWER); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + if(!mAdvancedAdvTxPower) { + alertDialogBuilder.setTitle(getString(R.string.title_config_radio_tx_power)); + } else { + alertDialogBuilder.setTitle(getString(R.string.title_config_adv_radio_tx_power)); + } + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_dialog_radio_tx_power, null); + final TextView currentTxPower = (TextView) view.findViewById(R.id.current_radio_tx_power); + currentTxPower.setText(mRadioTxPower); + final TextView currentTxPowerTitle = (TextView) view.findViewById(R.id.radio_tx_power_title); + radioTxPower = (EditText) view.findViewById(R.id.radio_tx_power); + final AlertDialog alertDialog = alertDialogBuilder.setView(view).setPositiveButton(getString(R.string.configure), null).setNegativeButton(getString(R.string.cancel), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + + if(mAdvancedAdvTxPower) { + radioTxPower.setHint(getString(R.string.advanced_adv_tx_power)); + currentTxPowerTitle.setText(getString(R.string.current_advanced_adv_tx_power)); + } + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(validateInput()){ + final byte [] radioTxPower = getValueFromView(); + ((OnRadioTxPowerListener)getParentFragment()).configureRadioTxPower(radioTxPower, mAdvancedAdvTxPower); + dismiss(); + } + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return alertDialog; + } + + private boolean validateInput() { + final String advertisingInterval = radioTxPower.getText().toString().trim(); + + final String txPower = radioTxPower.getText().toString().trim(); + if(advertisingInterval.isEmpty()){ + radioTxPower.setError("Enter supported Radio Tx power value"); + return false; + } else { + if (!txPower.matches(PATTERN_TX_POWER)) { + radioTxPower.setError("Please enter a valid value for Radio Tx power"); + return false; + } + } + return true; + } + + private byte[] getValueFromView() { + final byte [] data = new byte[1]; + data[0] = (byte) Integer.parseInt(radioTxPower.getText().toString().trim()); + return data; + } + + @Override + public void onDetach() { + super.onDetach(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ReadWriteAdvertisementSlotDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ReadWriteAdvertisementSlotDialogFragment.java new file mode 100644 index 0000000..5f2f668 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/ReadWriteAdvertisementSlotDialogFragment.java @@ -0,0 +1,587 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.URLUtil; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.auth.GoogleAuthException; +import com.google.android.gms.auth.GoogleAuthUtil; +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import no.nordicsemi.android.nrfbeacon.nearby.AuthorizedServiceTask; +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.util.AuthTaskUrlShortener; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +/** + * Created by rora on 07.03.2016. + */ +public class ReadWriteAdvertisementSlotDialogFragment extends DialogFragment { + + + private static final String PATTERN_NAMESPACE_ID = "[0-9a-fA-F]{20}"; + private static final String PATTERN_INSTANCE_ID = "[0-9a-fA-F]{12}"; + private static final String RW_ADVERTISEMENT_SLOT = "RW_ADVERTISEMENT_SLOT"; + private static final String SLOT_TASK = "SLOT_TASK"; + private static final String ACTIVE_SLOT = "ACTIVE_SLOT"; + private static final String TAG = "BEACON"; + private static final String ACCOUNT_NAME_PREF = "userAccount"; + private static final String SHARED_PREFS_NAME = "nrfNearbyInfo"; + private static final String AUTH_PROXIMITY_API = "oauth2:https://www.googleapis.com/auth/userlocation.beacon.registry"; + private static final String AUTH_SCOPE_URL_SHORTENER = "oauth2:https://www.googleapis.com/auth/urlshortener"; + + private static final int EMPTY_SLOT = -1; + private static final int TYPE_UID = 0x00; + private static final int TYPE_URL = 0x10; + private static final int TYPE_TLM = 0x20; + private static final int TYPE_EID = 0x30; + + + private byte [] mReadWriteAdvSlotData; + private int mActiveSlot; + private boolean mNewAddSlot; + + private LinearLayout mSlotStateContainer; + private LinearLayout mUidInfoContainer; + private LinearLayout mUrlInfoContainer; + private LinearLayout mUrlShortenerContainer; + private LinearLayout mEidInfoContainer; + private Spinner mFrameTypes; + private Spinner mUrlTypes; + private Spinner mSecurityTypes; + private TextView mActiveSlotNumber; + private TextView mSlotState; + private TextView mUrlShortText; + private EditText mNamespaceId; + private EditText mInstanceId; + private EditText mUrl; + private EditText mTimerExponent; + private int mFrameType; + private int mNewFrameType; + private Button mButtonNeutral; + private ProgressDialog mProgressDialog; + private Handler mProgressDialogHandler; + private boolean mUrlShortenerClicked = false; + + public interface OnReadWriteAdvertisementSlotListener { + void configureEidSlot(byte[] eidSlotData); + + void configureUidSlot(byte[] uidSlotData); + + void configureUrlSlot(byte[] urlSlotData); + + void configureTlmSlot(byte[] tlmSlotData); + } + + public static ReadWriteAdvertisementSlotDialogFragment newInstance(final boolean addNewSlot, final int activeSlot, byte [] readWriteAdvSlotData){ + ReadWriteAdvertisementSlotDialogFragment fragment = new ReadWriteAdvertisementSlotDialogFragment(); + final Bundle args = new Bundle(); + args.putBoolean(SLOT_TASK, addNewSlot); + args.putInt(ACTIVE_SLOT, activeSlot); + args.putByteArray(RW_ADVERTISEMENT_SLOT, readWriteAdvSlotData); + fragment.setArguments(args); + return fragment; + } + + public ReadWriteAdvertisementSlotDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mActiveSlot = getArguments().getInt(ACTIVE_SLOT); + mNewAddSlot = getArguments().getBoolean(SLOT_TASK); + mReadWriteAdvSlotData = getArguments().getByteArray(RW_ADVERTISEMENT_SLOT); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.title_rw_adv_slot)); + + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_dialog_rw_adv_slot, null); + mSlotStateContainer = (LinearLayout) view.findViewById(R.id.slot_state_container); + mUidInfoContainer = (LinearLayout) view.findViewById(R.id.uid_container); + mUrlInfoContainer = (LinearLayout) view.findViewById(R.id.url_container); + mUrlShortenerContainer = (LinearLayout) view.findViewById(R.id.url_shortener_container); + mEidInfoContainer = (LinearLayout) view.findViewById(R.id.eid_data_container); + mUrlTypes = (Spinner) view.findViewById(R.id.url_types); + mFrameTypes = (Spinner) view.findViewById(R.id.frame_types); + mSecurityTypes = (Spinner) view.findViewById(R.id.security_type); + mNamespaceId = (EditText) view.findViewById(R.id.namespace_id); + mInstanceId = (EditText) view.findViewById(R.id.instance_id); + mUrl = (EditText) view.findViewById(R.id.url_data); + mTimerExponent = (EditText) view.findViewById(R.id.timer_exponent); + mActiveSlotNumber = (TextView) view.findViewById(R.id.slot_number); + mSlotState = (TextView) view.findViewById(R.id.slot_state); + mUrlShortText = (TextView) view.findViewById(R.id.url_short_text); + + final AlertDialog alertDialog = alertDialogBuilder.setView(view) + .setPositiveButton(getString(R.string.configure), null) + .setNegativeButton(getString(R.string.cancel), null) + .setNeutralButton(getString(R.string.random_uid), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setCancelable(false); + mProgressDialog.setCanceledOnTouchOutside(false); + mProgressDialog.setTitle(getString(R.string.prog_dialog_url_shortener)); + mProgressDialog.setMessage(getString(R.string.prog_dialog_url_shortener_message)); + + mFrameTypes.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + switch (position) { + case 0: + mButtonNeutral.setVisibility(View.VISIBLE); + mButtonNeutral.setText(getString(R.string.random_uid)); + mUidInfoContainer.setVisibility(View.VISIBLE); + mUrlInfoContainer.setVisibility(View.GONE); + mUrlShortenerContainer.setVisibility(View.GONE); + mEidInfoContainer.setVisibility(View.GONE); + if (TYPE_UID == mFrameType) { + final String namespaceId = ParserUtils.bytesToHex(mReadWriteAdvSlotData, 2, 10, false); + final String instanceId = ParserUtils.bytesToHex(mReadWriteAdvSlotData, 11, 6, false); + mNamespaceId.setText(namespaceId); + mInstanceId.setText(instanceId); + } + mNewFrameType = TYPE_UID; + break; + case 1: + mButtonNeutral.setVisibility(View.VISIBLE); + mButtonNeutral.setText(getString(R.string.url_shorten_button)); + mUrlInfoContainer.setVisibility(View.VISIBLE); + mUrlShortenerContainer.setVisibility(View.GONE); + mUidInfoContainer.setVisibility(View.GONE); + mEidInfoContainer.setVisibility(View.GONE); + if (TYPE_URL == mFrameType) { + String url = ParserUtils.decodeUri(mReadWriteAdvSlotData, 2, mReadWriteAdvSlotData.length - 2); + if(url.startsWith("https://www.")){ + url = url.replace("https://www.", ""); + mUrlTypes.setSelection(0); + } else if(url.startsWith("http://www.")){ + url = url.replace("http://www.", ""); + mUrlTypes.setSelection(1); + } else if(url.startsWith("https://")){ + url = url.replace("https://", ""); + mUrlTypes.setSelection(2); + } else if(url.startsWith("https://")){ + url = url.replace("http://", ""); + mUrlTypes.setSelection(3); + } + mUrl.setText(url.trim()); + + + } + mNewFrameType = TYPE_URL; + break; + case 2: + mButtonNeutral.setVisibility(View.GONE); + mUidInfoContainer.setVisibility(View.GONE); + mUrlInfoContainer.setVisibility(View.GONE); + mUrlShortenerContainer.setVisibility(View.GONE); + mEidInfoContainer.setVisibility(View.GONE); + mNewFrameType = TYPE_TLM; + break; + case 3: + mButtonNeutral.setVisibility(View.GONE); + mEidInfoContainer.setVisibility(View.VISIBLE); + mUidInfoContainer.setVisibility(View.GONE); + mUrlInfoContainer.setVisibility(View.GONE); + mUrlShortenerContainer.setVisibility(View.GONE); + if (TYPE_EID == mFrameType) { + final String timerExponent = String.valueOf(ParserUtils.getIntValue(mReadWriteAdvSlotData, 1, BluetoothGattCharacteristic.FORMAT_UINT8)); + mTimerExponent.setText(timerExponent); + } + mNewFrameType = TYPE_EID; + break; + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + if(mReadWriteAdvSlotData.length > 0) + mSecurityTypes.setSelection(0); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (validateInput()) { + byte[] data = getValueFromView(); + switch (mFrameTypes.getSelectedItemPosition()) { + case 0: + ((OnReadWriteAdvertisementSlotListener) getParentFragment()).configureUidSlot(data); + break; + case 1: + ((OnReadWriteAdvertisementSlotListener) getParentFragment()).configureUrlSlot(data); + break; + case 2: + ((OnReadWriteAdvertisementSlotListener) getParentFragment()).configureTlmSlot(data); + break; + case 3: + ((OnReadWriteAdvertisementSlotListener) getParentFragment()).configureEidSlot(data); + break; + } + dismiss(); + } + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + mButtonNeutral = alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL); + mButtonNeutral.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + switch (mFrameTypes.getSelectedItemPosition()) { + case 0: + final String randomUid = ParserUtils.randomUid(16); + final String namespace = (randomUid.substring(0, 20)); + final String instance = (randomUid.substring(20, randomUid.length())); + mNamespaceId.setText(namespace.toUpperCase()); + mInstanceId.setText(instance.toUpperCase()); + break; + case 1: + final String url = mUrl.getText().toString().trim(); + + if (!url.isEmpty()) { + mUrlShortenerClicked = true; + /*mProgressDialog.show(); + mProgressDialogHandler = new Handler(); + mProgressDialogHandler.postDelayed(mRunnableHandler, 15000);*/ + if(getUserAccount() != null) + new AuthTaskUrlShortener(mUrlShortenerCallback, url, getActivity(), getUserAccount()).execute(); + else + Toast.makeText(getActivity(), getString(R.string.user_account_unavailable), Toast.LENGTH_LONG).show(); + + } + break; + } + } + }); + + mUrl.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mUrl.setError(null); + if(mUrlShortenerContainer.getVisibility() == View.VISIBLE) + mUrlShortenerContainer.setVisibility(View.GONE); + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + updateUi(); + + final String name = getUserAccountName(); + if(!name.isEmpty()) { + Account userAccount = null; + final Account[] accounts = AccountManager.get(getActivity()).getAccounts(); + for (Account account : accounts) { + if (account.name.equals(name)) { + userAccount = account; + break; + } + } + new AuthorizedServiceTask(getActivity(), userAccount, AUTH_SCOPE_URL_SHORTENER).execute(); + } + return alertDialog; + } + + private String getUserAccountName(){ + SharedPreferences mSharedPreferences = getContext().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + return mSharedPreferences.getString(ACCOUNT_NAME_PREF, null); + } + + private Account getUserAccount(){ + final String name = getUserAccountName(); + if(!name.isEmpty()) { + final Account[] accounts = AccountManager.get(getActivity()).getAccounts(); + for (Account account : accounts) { + if (account.name.equals(name)) { + return account; + } + } + } + return null; + } + + private final Runnable mRunnableHandler = new Runnable() { + @Override + public void run() { + if(mProgressDialog != null && mProgressDialog.isShowing()){ + mProgressDialog.dismiss(); + } + } + }; + + private boolean validateInput() { + + if(mUidInfoContainer.getVisibility() == View.VISIBLE) { + final String namespaceId = mNamespaceId.getText().toString().trim(); + if (namespaceId.isEmpty()) { + mNamespaceId.setError("Please enter namespace id"); + return false; + } else { + if (!namespaceId.matches(PATTERN_NAMESPACE_ID)) { + mNamespaceId.setError("Please enter a valid value for namespace id"); + return false; + } + } + final String instanceId = mInstanceId.getText().toString().trim(); + if (instanceId.isEmpty()) { + mInstanceId.setError("Please enter instance id"); + return false; + } else { + if (!instanceId.matches(PATTERN_INSTANCE_ID)) { + mInstanceId.setError("Please enter a valid value for instance id"); + return false; + } + } + } + + if(mUrlInfoContainer.getVisibility() == View.VISIBLE) { + + if(mUrlShortenerContainer.getVisibility() == View.VISIBLE){ + return true; + } + + final String url = mUrlTypes.getSelectedItem().toString().trim() + mUrl.getText().toString().trim(); + if (url.isEmpty()) { + mUrl.setError("Please enter a value for URL"); + return false; + } else { + if (!URLUtil.isValidUrl(url)) { + mUrl.setError("Please enter a valid value for URL"); + return false; + } else if (!URLUtil.isNetworkUrl(url)) { + mUrl.setError("Please enter a valid value for URL"); + return false; + } else if (ParserUtils.encodeUri(url).length > 18){ + mUrl.setError("Please enter a shortened URL or press use the URL shortener!"); + return false; + } + } + } + + if(mEidInfoContainer.getVisibility() == View.VISIBLE) { + + final String timerExponent = mTimerExponent.getText().toString().trim(); + if(timerExponent.isEmpty()){ + mTimerExponent.setError(getString(R.string.timer_exponent_error)); + return false; + } else { + boolean valid; + int i = 0; + try { + i = Integer.parseInt(timerExponent); + valid = ((i & 0xFFFFFF00) == 0 || (i & 0xFFFFFF00) == 0xFFFFFF00); + } catch (NumberFormatException e) { + valid = false; + } + if (!valid) { + mTimerExponent.setError(getString(R.string.uint8_error)); + return false; + } else if ((i < 10 || i > 15)){ + mTimerExponent.setError(getString(R.string.timer_exponent_error_value)); + return false; + } + } + } + return true; + } + + private byte[] getValueFromView() { + byte [] data = null; + int length; + switch (mNewFrameType) { + case TYPE_UID: + data = new byte[17]; + data[0] = TYPE_UID; + final String namespaceId = mNamespaceId.getText().toString().trim(); + ParserUtils.setByteArrayValue(data, 1, namespaceId); + final String instanceId = mInstanceId.getText().toString().trim(); + ParserUtils.setByteArrayValue(data, 11, instanceId); + return data; + case TYPE_URL: + final String urlText; + if(mUrlShortenerContainer.getVisibility() != View.VISIBLE){ + urlText = mUrlTypes.getSelectedItem().toString().trim() + mUrl.getText().toString().trim(); + } else { + urlText = mUrlShortText.getText().toString().trim(); + } + + byte [] urlData = ParserUtils.encodeUri(urlText); + data = new byte[urlData.length + 1]; + data[0] = (byte) TYPE_URL; + System.arraycopy(urlData, 0, data , 1, urlData.length); + return data; + case TYPE_TLM: + data = new byte[1]; + data[0] = TYPE_TLM; + return data; + case TYPE_EID: + final String timerExponent = mTimerExponent.getText().toString().trim(); + + if (mSecurityTypes.getSelectedItemPosition() == 0) { + data = new byte[34]; + data[0] = TYPE_EID; + data[33] = (byte) Integer.parseInt(timerExponent); + return data; + } else { + data = new byte[18]; + data[0] = TYPE_EID; + data[17] = (byte) Integer.parseInt(timerExponent); + return data; + } + } + return data; + } + + private void updateUi() { + final int frameType; + if(mReadWriteAdvSlotData == null || mReadWriteAdvSlotData.length == 0) + frameType = EMPTY_SLOT; + else + frameType = ParserUtils.getIntValue(mReadWriteAdvSlotData, 0, BluetoothGattCharacteristic.FORMAT_UINT8); + switch (frameType){ + case TYPE_UID: + mButtonNeutral.setVisibility(View.VISIBLE); + mFrameType = TYPE_UID; + mFrameTypes.setSelection(0); + break; + case TYPE_URL: + mButtonNeutral.setVisibility(View.VISIBLE); + mButtonNeutral.setText(getString(R.string.url_shorten_button)); + mFrameType = TYPE_URL; + mFrameTypes.setSelection(1); + break; + case TYPE_TLM: + mButtonNeutral.setVisibility(View.GONE); + mFrameType = TYPE_TLM; + mFrameTypes.setSelection(2); + break; + case TYPE_EID: + mButtonNeutral.setVisibility(View.GONE); + mFrameType = TYPE_EID; + mFrameTypes.setSelection(3); + break; + case EMPTY_SLOT: + mSlotStateContainer.setVisibility(View.VISIBLE); + mSlotState.setText(getString(R.string.slot_state_empty)); + mFrameType = EMPTY_SLOT; + break; + } + mActiveSlotNumber.setText(String.valueOf(mActiveSlot)); + } + + @Override + public void onDetach() { + super.onDetach(); + } + + private final Callback mUrlShortenerCallback = new Callback() { + @Override + public void onFailure(Request request, IOException e) { + Log.v(TAG, "Failure: " + request.toString()); + if(mProgressDialog != null && mProgressDialog.isShowing()) + mProgressDialog.dismiss(); + } + + @Override + public void onResponse(Response response) throws IOException { + if(mProgressDialog != null && mProgressDialog.isShowing()) + mProgressDialog.dismiss(); + if(!response.isSuccessful()){ + //GoogleAuthUtil.clearToken(getActivity(), token); + mUrlShortenerContainer.setVisibility(View.GONE); + Toast.makeText(getActivity(), response.body().string(), Toast.LENGTH_SHORT).show(); + Account userAccount = getUserAccount(); + if(userAccount != null) + new AuthorizedServiceTask(getActivity(), userAccount, AUTH_SCOPE_URL_SHORTENER).execute(); + } else { + try { + mUrlShortenerContainer.setVisibility(View.VISIBLE); + mUrl.setError(null); + final JSONObject json = new JSONObject(response.body().string()); + mUrlShortText.setText(json.getString("id")); + + } catch (JSONException e) { + e.printStackTrace(); + } + + } + } + }; +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RegisterBeaconDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RegisterBeaconDialogFragment.java new file mode 100644 index 0000000..095a1d5 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RegisterBeaconDialogFragment.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; + +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +/** + * Created by rora on 07.03.2016. + */ +public class RegisterBeaconDialogFragment extends DialogFragment { + + private static final String PATTERN_NAMESPACE_ID = "[0-9a-fA-F]{20}"; + private static final String PATTERN_INSTANCE_ID = "[0-9a-fA-F]{12}"; + private static final String TAG = "BEACON"; + private static final int TYPE_UID = 0x00; + private static final int TYPE_EID = 0x30; + private EditText mNamespaceId; + private EditText mInstanceId; + private EditText mAttachment; + + public interface OnBeaconRegisteredListener { + void registerBeaconListener(final byte[] uid); + } + + public static RegisterBeaconDialogFragment newInstance(){ + RegisterBeaconDialogFragment fragment = new RegisterBeaconDialogFragment(); + return fragment; + } + + public RegisterBeaconDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.register_beacon_title)); + alertDialogBuilder.setMessage(getString(R.string.register_beacon_message)); + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_register_beacon, null); + mNamespaceId = (EditText) view.findViewById(R.id.namespace_id); + mInstanceId = (EditText) view.findViewById(R.id.instance_id); + + final AlertDialog alertDialog = alertDialogBuilder.setView(view) + .setPositiveButton(getString(R.string.register), null) + .setNeutralButton(getString(R.string.random_uid), null) + .setNegativeButton(getString(R.string.cancel), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (validateInput()) { + final byte[] uid = new byte[16]; + ParserUtils.setByteArrayValue(uid, 0, mNamespaceId.getText().toString().trim()); + ParserUtils.setByteArrayValue(uid, 10, mInstanceId.getText().toString().trim()); + ((OnBeaconRegisteredListener) getParentFragment()).registerBeaconListener(uid); + dismiss(); + } + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final String randomUid = ParserUtils.randomUid(16); + final String namespace = (randomUid.substring(0,20)); + final String instance = (randomUid.substring(20,randomUid.length())); + mNamespaceId.setText(namespace.toUpperCase()); + mInstanceId.setText(instance.toUpperCase()); + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + return alertDialog; + } + + @Override + public void onDetach() { + super.onDetach(); + } + + private boolean validateInput(){ + final String namespaceId = mNamespaceId.getText().toString().trim(); + if (namespaceId.isEmpty()) { + mNamespaceId.setError("Please enter namespace id"); + return false; + } else { + if (!namespaceId.matches(PATTERN_NAMESPACE_ID)) { + mNamespaceId.setError("Please enter a valid value for namespace id"); + return false; + } + + } + final String instanceId = mInstanceId.getText().toString().trim(); + if (instanceId.isEmpty()) { + mInstanceId.setError("Please enter instance id"); + return false; + } else { + if (!instanceId.matches(PATTERN_INSTANCE_ID)) { + mInstanceId.setError("Please enter a valid value for instance id"); + return false; + } + + } + return true; + } + + private byte[] aes128Encrypt(byte[] data, SecretKeySpec keySpec) { + Cipher cipher; + try { + // Ignore the "ECB encryption should not be used" warning. We use exactly one block so + // the difference between ECB and CBC is just an IV or not. In addition our blocks are + // always different since they have a monotonic timestamp. Most importantly, our blocks + // aren't sensitive. Decrypting them means means knowing the beacon time and its rotation + // period. If due to ECB an attacker could find out that the beacon broadcast the same + // block a second time, all it could infer is that for some reason the clock of the beacon + // reset, which is not very helpful + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + Log.e(TAG, "Error constructing cipher instance", e); + return null; + } + + try { + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + } catch (InvalidKeyException e) { + Log.e(TAG, "Error initializing cipher instance", e); + return null; + } + + byte[] ret; + try { + ret = cipher.doFinal(data); + } catch (IllegalBlockSizeException | BadPaddingException e) { + Log.e(TAG, "Error executing cipher", e); + return null; + } + + return ret; + } + + + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RemainConnectableDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RemainConnectableDialogFragment.java new file mode 100644 index 0000000..2f85468 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/RemainConnectableDialogFragment.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.SwitchCompat; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +/** + * Created by rora on 07.03.2016. + */ +public class RemainConnectableDialogFragment extends DialogFragment { + + private static final String PATTERN_TX_POWER = "[0-9a-fA-F]{32}"; + private static final String REMAIN_CONNECTABLE_FLAG = "REMAIN_CONNECTABLE_FLAG"; + private static final String TAG = "BEACON"; + private boolean mRemainConnectableFlag; + + public interface OnBeaconUnlockListener { + void unlockBeacon(byte[] encryptedLockCode, final byte[] beaconLockCode); + } + + public static RemainConnectableDialogFragment newInstance(final boolean flag){ + RemainConnectableDialogFragment fragment = new RemainConnectableDialogFragment(); + final Bundle args = new Bundle(); + args.putBoolean(REMAIN_CONNECTABLE_FLAG, flag); + fragment.setArguments(args); + return fragment; + } + + public RemainConnectableDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mRemainConnectableFlag = getArguments().getBoolean(REMAIN_CONNECTABLE_FLAG); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.title_remain_connectable)); + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_remain_connectable, null); + final AlertDialog alertDialog = alertDialogBuilder.setView(view).setPositiveButton(getString(R.string.ok), null).setNegativeButton(getString(R.string.cancel), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + final SwitchCompat switchCompat = (SwitchCompat) view.findViewById(R.id.remain_connectable); + switchCompat.setChecked(mRemainConnectableFlag); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return alertDialog; + } + + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/UnlockBeaconDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/UnlockBeaconDialogFragment.java new file mode 100644 index 0000000..fa3eb92 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/UnlockBeaconDialogFragment.java @@ -0,0 +1,149 @@ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +/** + * Created by rora on 07.03.2016. + */ +public class UnlockBeaconDialogFragment extends DialogFragment { + + private static final String PATTERN_TX_POWER = "[0-9a-fA-F]{32}"; + private static final String CHALLENGE = "CHALLENGE"; + private static final String TAG = "BEACON"; + private EditText mUnlockCode; + private OnBeaconUnlockListener beaconUnlockListener; + private byte [] mChallenge; + + public interface OnBeaconUnlockListener { + void unlockBeacon(byte [] encryptedLockCode, final byte [] beaconLockCode); + void cancelUnlockBeacon(); + } + + public static UnlockBeaconDialogFragment newInstance(byte [] challenge){ + UnlockBeaconDialogFragment fragment = new UnlockBeaconDialogFragment(); + final Bundle args = new Bundle(); + args.putByteArray(CHALLENGE, challenge); + fragment.setArguments(args); + return fragment; + } + + public UnlockBeaconDialogFragment(){ + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(getArguments() != null){ + mChallenge = getArguments().getByteArray(CHALLENGE); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setTitle(getString(R.string.unlock_beacon_title)); + alertDialogBuilder.setMessage(getString(R.string.unlock_beacon_message)); + final View alertDialogView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_dialog_unlock, null); + mUnlockCode = (EditText) alertDialogView.findViewById(R.id.lock_code); + mUnlockCode.setText("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + + final AlertDialog alertDialog = alertDialogBuilder.setView(alertDialogView).setPositiveButton(getString(R.string.unlock), null).setNegativeButton(getString(R.string.cancel), null).show(); + alertDialog.setCanceledOnTouchOutside(false); + + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(validateInput()) { + final String lockCode = mUnlockCode.getText().toString().trim(); + byte [] beaconLockCode = new byte[16]; + ParserUtils.setByteArrayValue(beaconLockCode, 0, lockCode); + final byte [] encryptedLockCode = ParserUtils.aes128Encrypt(mChallenge, new SecretKeySpec(beaconLockCode, "AES")); + ((OnBeaconUnlockListener)getParentFragment()).unlockBeacon(encryptedLockCode, beaconLockCode); + } + } + }); + + alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + ((OnBeaconUnlockListener)getParentFragment()).cancelUnlockBeacon(); + } + }); + + return alertDialog; + } + + @Override + public void onDetach() { + super.onDetach(); + } + + private boolean validateInput(){ + final String lockCode = mUnlockCode.getText().toString().trim(); + if(lockCode.isEmpty()){ + mUnlockCode.setError("Please enter the lock code to unlock the beacon"); + return false; + } else if (!lockCode.matches(PATTERN_TX_POWER)) { + mUnlockCode.setError("Please enter a valid value for new lock code"); + return false; + } + return true; + } + + private byte[] aes128Encrypt(byte[] data, SecretKeySpec keySpec) { + Cipher cipher; + try { + // Ignore the "ECB encryption should not be used" warning. We use exactly one block so + // the difference between ECB and CBC is just an IV or not. In addition our blocks are + // always different since they have a monotonic timestamp. Most importantly, our blocks + // aren't sensitive. Decrypting them means means knowing the beacon time and its rotation + // period. If due to ECB an attacker could find out that the beacon broadcast the same + // block a second time, all it could infer is that for some reason the clock of the beacon + // reset, which is not very helpful + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + Log.e(TAG, "Error constructing cipher instance", e); + return null; + } + + try { + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + } catch (InvalidKeyException e) { + Log.e(TAG, "Error initializing cipher instance", e); + return null; + } + + byte[] ret; + try { + ret = cipher.doFinal(data); + } catch (IllegalBlockSizeException | BadPaddingException e) { + Log.e(TAG, "Error executing cipher", e); + return null; + } + + return ret; + } + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/UpdateFragment.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/UpdateFragment.java new file mode 100644 index 0000000..63ada17 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/update/UpdateFragment.java @@ -0,0 +1,1592 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.update; + +import android.Manifest; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Activity; +import android.app.ProgressDialog; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; +import android.text.SpannableString; +import android.text.style.UnderlineSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.common.AccountPicker; +import com.google.sample.libeddystoneeidr.EddystoneEidrGenerator; +import com.google.sample.libproximitybeacon.ProximityBeaconImpl; +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Random; +import java.util.UUID; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import no.nordicsemi.android.nrfbeacon.nearby.AuthorizedServiceTask; +import no.nordicsemi.android.nrfbeacon.nearby.MainActivity; +import no.nordicsemi.android.nrfbeacon.nearby.R; +import no.nordicsemi.android.nrfbeacon.nearby.UpdateService; +import no.nordicsemi.android.nrfbeacon.nearby.common.BaseFragment; +import no.nordicsemi.android.nrfbeacon.nearby.scanner.ScannerFragment; +import no.nordicsemi.android.nrfbeacon.nearby.scanner.ScannerFragmentListener; +import no.nordicsemi.android.nrfbeacon.nearby.util.NetworkUtils; +import no.nordicsemi.android.nrfbeacon.nearby.util.ParserUtils; + +//import eidr.EddystoneEidrGenerator; + +public class UpdateFragment extends BaseFragment implements ScannerFragmentListener, + UnlockBeaconDialogFragment.OnBeaconUnlockListener, + ReadWriteAdvertisementSlotDialogFragment.OnReadWriteAdvertisementSlotListener, + LockStateDialogFragment.OnLockStateListener, + RegisterBeaconDialogFragment.OnBeaconRegisteredListener, + CreateAttachmentDialogFragment.OnAttachmentCreatedListener, + RadioTxPowerDialogFragment.OnRadioTxPowerListener, + ClearSlotDialogFragment.OnClearSlotListener, + AdvertisingIntervalDialogFragment.OnAdvertisingIntervalListener { + /** + * The UUID of a service in the beacon advertising packet when in Config mode. This may be null if no filter required. + */ +// private static final UUID EDDYSTONE_GATT_CONFIG_SERVICE_UUID = UUID.fromString("A3C87500-8ED3-4BDF-8A39-A01BEBEDE295"); + private static final UUID EDDYSTONE_GATT_CONFIG_SERVICE_UUID = UUID.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"); + private static final String AUTH_SCOPE_PROXIMITY_API = "oauth2:https://www.googleapis.com/auth/userlocation.beacon.registry"; + private static final String AUTH_SCOPE_URL_SHORTENER = "oauth2:https://www.googleapis.com/auth/urlshortener"; + private static final String APP_NAMESPACE_TYPE = "nrf-nearby-1100/string"; + private static final String ACCOUNT_NAME_PREF = "userAccount"; + private static final String SHARED_PREFS_NAME = "nrfNearbyInfo"; + private static final String SERVICE_ECDH_KEY = "SERVICE_ECDH_KEY"; + private static final String TAG = "BEACON"; + + static final int REQUEST_CODE_USER_ACCOUNT = 1002; + private static final int REQUEST_PERMISSION_REQ_CODE = 76; // any 8-bit number + private static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR = 1003; + private static final int REQUEST_ENABLE_BT = 1; + + private static final int LOCKED = 0x00; + private static final int UNLOCKED = 0x01; + private static final int UNLOCKED_AUTOMATIC_RELOCK_DISABLED = 0x02; + private static final int IS_VARIABLE_TX_POWER_SUPPORTED = 0x02; + private static final int EMPTY_SLOT = -1; + private static final int TYPE_UID = 0x00; + private static final int TYPE_URL = 0x10; + private static final int TYPE_TLM = 0x20; + private static final int TYPE_EID = 0x30; + + private static final int ERROR_ALREADY_EXISTS = 409; + private static final int ERROR_UNAUTHORIZED = 401; + + private TextView mBeaconHelp; + + private Button mConnectButton; + private LinearLayout mBeaconConfigurationContainer; + private LinearLayout mFrameTypeContainer; + private LinearLayout mUidDataContainer; + private LinearLayout mUrlDataContainer; + private LinearLayout mTlmDataContainer; + private LinearLayout mEtlmDataContainer; + private LinearLayout mEidDataContainer; + private ImageView mShowBroadcastCapabilities; + private ImageView mEditSlot; + private ImageView mEditAdvInterval; + private ImageView mEditRadioTxPower; + private ImageView mShowSlotInfo; + private TextView mFrameTypeView; + private Spinner mActiveSlots; + private TextView mNamespaceId; + private TextView mInstanceId; + private TextView mUrl; + private TextView mEtlm; + private TextView mEtlmSalt; + private TextView mEtlmMessageIntCheck; + private TextView mVoltage; + private TextView mTemperature; + private TextView mPduCount; + private TextView mTimeSinceReboot; + private TextView mTimerExponent; + private TextView mClockValue; + private TextView mEid; + private TextView mAdvertisingInterval; + private TextView mRadioTxPower; + + private UpdateService.ServiceBinder mBinder; + private boolean mBounnd; + private ProximityBeaconImpl mProximityApiClient; + private UnlockBeaconDialogFragment mUnlockBeaconDialogFragment = null; + private boolean mIsBeaconLocked = true; + private byte[] mBroadcastCapabilities; + private byte[] mRwAdvertisingSlot; + private ArrayList mMaxSupportedSlots; + private ArrayAdapter mMaxActiveSlotsAdapter; + private int mActiveSlot; + private boolean mIsActiveSlotAdapterUpdated = false; + private int mCurrentLockState; + private EddystoneEidrGenerator generator; + private byte[] mUnlockCode; + private ProgressDialog mProgressDialog; + private int mFrameType; + private byte[] mRadioTxPowerData; + private byte[] mDecryptedIdentityKey; + private byte[] mEncryptedIdentityKey; + private byte[] mServiceEcdhKey; + private byte[] mBeaconPublicEcdhKey = null; + private Handler mProgressDialogHandler; + private String mBeaconEcdhPrivateKey; + private boolean mEikGenerated = false; + private int mAdvancedAdvTxPower; + private String mAccountName; + private ArrayList mActiveSlotsTypes; + private boolean mRemainConnectable = false; + private byte[] mUidForEid; + private Context mContext; + + public void ensurePermissionGranted(final String[] permissions) { + ensurePermission(permissions); + } + + private ServiceConnection mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(final ComponentName name, final IBinder service) { + final UpdateService.ServiceBinder binder = mBinder = (UpdateService.ServiceBinder) service; + final int state = binder.getState(); + switch (state) { + case UpdateService.STATE_DISCONNECTED: + binder.connect(); + break; + case UpdateService.STATE_CONNECTED: + if (!hasOptionsMenu()) { + setHasOptionsMenu(true); + getActivity().invalidateOptionsMenu(); + } + mConnectButton.setText(R.string.action_disconnect); + //mBinder.startReadingCharacteristicsForActiveSlot(); + final Integer lockState = binder.getLockState(); + if (lockState != null) + updateUiForBeacons(BluetoothGatt.STATE_CONNECTED, lockState); + + mActiveSlotsTypes = binder.getAllSlotInformation(); + + mBroadcastCapabilities = mBinder.getBroadcastCapabilities(); + if (mBroadcastCapabilities != null) { + Log.v("BEACON", "Broadcast capabilities"); + int mMaxSupportedSlots = ParserUtils.getIntValue(mBroadcastCapabilities, 1, BluetoothGattCharacteristic.FORMAT_UINT8); + updateActiveSlotSpinner(mMaxSupportedSlots); + } + + mActiveSlot = binder.getActiveSlot(); + if (mActiveSlot > -1) { + mActiveSlots.setSelection(mActiveSlot); + } + + final Integer advertisingInterval = binder.getAdvInterval(); + if (advertisingInterval != null) + mAdvertisingInterval.setText(String.valueOf(advertisingInterval)); + + final Integer radioTxPower = binder.getRadioTxPower(); + if (radioTxPower != null) + mRadioTxPower.setText(String.valueOf(radioTxPower)); + + final Integer advanceAdvInterval = binder.getAdvancedAdvertisedTxPower(); + if (advanceAdvInterval != null) { + }//update ui + + final byte[] beaconPublicEcdhKey = binder.getBeaconPublicEcdhKey(); + if (beaconPublicEcdhKey != null && beaconPublicEcdhKey.length == 32) { + final String ecdhKey = ParserUtils.bytesToHex(beaconPublicEcdhKey, 0, 32, false); + mBeaconPublicEcdhKey = new byte[32]; + ParserUtils.setByteArrayValue(mBeaconPublicEcdhKey, 0, ecdhKey); + } + + final byte[] encryptedIdentityKey = binder.getIdentityKey(); + if (encryptedIdentityKey != null && encryptedIdentityKey.length == 16) { + mEncryptedIdentityKey = encryptedIdentityKey; + mUnlockCode = binder.getBeaconLockCode(); + if (mUnlockCode != null) { + ParserUtils.aes128decrypt(encryptedIdentityKey, new SecretKeySpec(mUnlockCode, "AES")); + mDecryptedIdentityKey = encryptedIdentityKey; + } + } + + final byte[] readWriteAdvSlot = binder.getReadWriteAdvSlotData(); + if (readWriteAdvSlot != null && readWriteAdvSlot.length > 0) { + mRwAdvertisingSlot = readWriteAdvSlot; + mFrameType = ParserUtils.getIntValue(readWriteAdvSlot, 0, BluetoothGattCharacteristic.FORMAT_UINT8); + updateUiWithFrameType(readWriteAdvSlot); + } + + } + } + + @Override + public void onServiceDisconnected(final ComponentName name) { + mBinder = null; + mIsActiveSlotAdapterUpdated = false; + } + }; + + private BroadcastReceiver mServiceBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final Activity activity = getActivity(); + if (activity == null || !isResumed()) + return; + + final String action = intent.getAction(); + + if (UpdateService.ACTION_STATE_CHANGED.equals(action)) { + final int state = intent.getIntExtra(UpdateService.EXTRA_DATA, UpdateService.STATE_DISCONNECTED); + + switch (state) { + case UpdateService.STATE_DISCONNECTED: + mConnectButton.setText(R.string.action_connect); + mConnectButton.setEnabled(true); + + final Intent service = new Intent(activity, UpdateService.class); + if(mBounnd) + activity.unbindService(mServiceConnection); + activity.stopService(service); + mBinder = null; + mBounnd = false; + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + mProgressDialogHandler.removeCallbacks(mRunnableHandler); + } + mEikGenerated = false; + mIsBeaconLocked = true; + updateUiForBeacons(UpdateService.STATE_DISCONNECTED, UpdateService.LOCKED); + break; + case UpdateService.STATE_CONNECTED: + mConnectButton.setText(R.string.action_disconnect); + mConnectButton.setEnabled(true); + break; + case UpdateService.STATE_DISCONNECTING: + case UpdateService.STATE_CONNECTING: + mConnectButton.setEnabled(false); + break; + } + } else if (UpdateService.ACTION_UNLOCK_BEACON.equals(action)) { + Log.v(TAG, "challenge Broadcast received in fragment: " + action); + final byte[] challenge = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + if (challenge != null && challenge.length == 16) { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + //mProgressDialog.dismiss(); + mProgressDialog.setMessage(getString(R.string.reading_all_slots)); + mProgressDialogHandler.removeCallbacks(mRunnableHandler); + } + mUnlockBeaconDialogFragment = UnlockBeaconDialogFragment.newInstance(challenge); + mUnlockBeaconDialogFragment.show(getChildFragmentManager(), null); + } + } else if (UpdateService.ACTION_BROADCAST_CAPABILITIES.equals(action)) { + /*final UUID uuid = ((ParcelUuid) intent.getParcelableExtra(UpdateService.EXTRA_DATA)).getUuid(); + mUuidView.setText(uuid.toString()); + setUuidControlsEnabled(true);*/ + mBroadcastCapabilities = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + if (mBroadcastCapabilities != null) { + Log.v("BEACON", "Broadcast capabilities"); + int mMaxSupportedSlots = ParserUtils.getIntValue(mBroadcastCapabilities, 1, BluetoothGattCharacteristic.FORMAT_UINT8); + updateActiveSlotSpinner(mMaxSupportedSlots); + } + } else if (UpdateService.ACTION_ACTIVE_SLOT.equals(action)) { + mActiveSlot = intent.getIntExtra(UpdateService.EXTRA_DATA, 0); + Log.v("BEACON", "Active slot: " + mActiveSlot); + } else if (UpdateService.ACTION_ADVERTISING_INTERVAL.equals(action)) { + final int advInterval = intent.getIntExtra(UpdateService.EXTRA_DATA, 0); + mAdvertisingInterval.setText(String.valueOf(advInterval) + " ms"); + } else if (UpdateService.ACTION_RADIO_TX_POWER.equals(action)) { + final byte[] radioTxPower = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + mRadioTxPowerData = radioTxPower; + mRadioTxPower.setText(ParserUtils.parse(radioTxPower, 0, radioTxPower.length, getString(R.string.update_rssi_unit))); + } else if (UpdateService.ACTION_ADVANCED_ADVERTISED_TX_POWER.equals(action)) { + mAdvancedAdvTxPower = intent.getIntExtra(UpdateService.EXTRA_DATA, 0); + } else if (UpdateService.ACTION_LOCK_STATE.equals(action)) { + updateUiForBeacons(BluetoothProfile.STATE_CONNECTED, intent.getIntExtra(UpdateService.EXTRA_DATA, 0)); + } else if (UpdateService.ACTION_UNLOCK.equals(action)) { + final byte[] data = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + } else if (UpdateService.ACTION_ECDH_KEY.equals(action)) { + final byte[] data = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + if (!mEikGenerated && data.length == 32) { + final String ecdhKey = ParserUtils.bytesToHex(data, 0, 32, false); + mBeaconPublicEcdhKey = new byte[32]; + ParserUtils.setByteArrayValue(mBeaconPublicEcdhKey, 0, ecdhKey); + } + } else if (UpdateService.ACTION_EID_IDENTITY_KEY.equals(action)) { + byte[] data = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + mEncryptedIdentityKey = data; + if(mUnlockCode != null) { + data = ParserUtils.aes128decrypt(data, new SecretKeySpec(mUnlockCode, "AES")); + mDecryptedIdentityKey = data; + } else { + if (mBinder != null ){ + mUnlockCode = mBinder.getBeaconLockCode(); + data = ParserUtils.aes128decrypt(data, new SecretKeySpec(mUnlockCode, "AES")); + mDecryptedIdentityKey = data; + } + } + } else if (UpdateService.ACTION_READ_WRITE_ADV_SLOT.equals(action)) { + if (mProgressDialog != null && mProgressDialog.isShowing()) + mProgressDialog.dismiss(); + mFrameType = intent.getIntExtra(UpdateService.EXTRA_FRAME_TYPE, -1); + updateUiWithFrameType(intent); + } else if (UpdateService.ACTION_ADVANCED_FACTORY_RESET.equals(action)) { + final byte[] data = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + } else if (UpdateService.ACTION_ADVANCED_REMAIN_CONNECTABLE.equals(action)) { + mRemainConnectable = intent.getExtras().getBoolean(UpdateService.EXTRA_DATA); + } else if (UpdateService.ACTION_BROADCAST_ALL_SLOT_INFO.equals(action)) { + mActiveSlotsTypes = intent.getStringArrayListExtra(UpdateService.EXTRA_DATA); + ensurePermission(new String [] {Manifest.permission.GET_ACCOUNTS}); + Log.v(TAG, "SLot info list size: " + mActiveSlotsTypes.size()); + } else if (UpdateService.ACTION_DONE.equals(action)) { + final boolean advanced = intent.getBooleanExtra(UpdateService.EXTRA_DATA, false); + mBinder.read(); + } else if (UpdateService.ACTION_GATT_ERROR.equals(action)) { + final int error = intent.getIntExtra(UpdateService.EXTRA_DATA, 0); + switch (error) { + case UpdateService.ERROR_UNSUPPORTED_DEVICE: + Toast.makeText(activity, R.string.update_error_device_not_supported, Toast.LENGTH_SHORT).show(); + break; + default: + Toast.makeText(activity, getString(R.string.update_error_other, error), Toast.LENGTH_SHORT).show(); + break; + } + mBinder.disconnectAndClose(); + } + } + }; + + private void updateUiWithFrameType(Intent intent) { + switch (mFrameType) { + case TYPE_UID: + mFrameTypeView.setText(getString(R.string.type_uid)); + final String namespaceId = intent.getExtras().getString(UpdateService.EXTRA_NAMESPACE_ID, ""); + final String instanceId = intent.getExtras().getString(UpdateService.EXTRA_INSTANCE_ID, ""); + mRwAdvertisingSlot = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + mUidDataContainer.setVisibility(View.VISIBLE); + mUrlDataContainer.setVisibility(View.GONE); + mTlmDataContainer.setVisibility(View.GONE); + mEtlmDataContainer.setVisibility(View.GONE); + mEidDataContainer.setVisibility(View.GONE); + mNamespaceId.setText(namespaceId); + mInstanceId.setText(instanceId); + break; + + case TYPE_URL: + mFrameTypeView.setText(getString(R.string.type_url)); + final String url = intent.getExtras().getString(UpdateService.EXTRA_URL, ""); + mRwAdvertisingSlot = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + mUrlDataContainer.setVisibility(View.VISIBLE); + mUidDataContainer.setVisibility(View.GONE); + mTlmDataContainer.setVisibility(View.GONE); + mEtlmDataContainer.setVisibility(View.GONE); + mEidDataContainer.setVisibility(View.GONE); + + final SpannableString urlAttachment = new SpannableString(url); + urlAttachment.setSpan(new UnderlineSpan(), 0, url.length(), 0); + mUrl.setText(urlAttachment); + //mUrl.setText(url); + break; + + case TYPE_TLM: + mEidDataContainer.setVisibility(View.GONE); + mUrlDataContainer.setVisibility(View.GONE); + mUidDataContainer.setVisibility(View.GONE); + + mRwAdvertisingSlot = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + final boolean containsEid = mActiveSlotsTypes.contains(getString(R.string.type_eid)); + if (containsEid) { + mEtlmDataContainer.setVisibility(View.VISIBLE); + mFrameTypeView.setText(getString(R.string.type_etlm)); + final String etlm = ParserUtils.bytesToHex(mRwAdvertisingSlot, 2, 12, true); + final String salt = ParserUtils.bytesToHex(mRwAdvertisingSlot, 14, 2, true); + final String messageIntegrityCheck = ParserUtils.bytesToHex(mRwAdvertisingSlot, 16, 2, true); + + mEtlm.setText(etlm); + mEtlmSalt.setText(salt); + mEtlmMessageIntCheck.setText(messageIntegrityCheck); + } else { + mFrameTypeView.setText(getString(R.string.type_tlm)); + final String batteryVoltage = intent.getExtras().getString(UpdateService.EXTRA_VOLTAGE, ""); + final String beaconTemperature = intent.getExtras().getString(UpdateService.EXTRA_BEACON_TEMPERATURE, ""); + final String pduCount = intent.getExtras().getString(UpdateService.EXTRA_PDU_COUNT, ""); + final String timeSinceBoot = intent.getExtras().getString(UpdateService.EXTRA_TIME_SINCE_BOOT, ""); + + mTlmDataContainer.setVisibility(View.VISIBLE); + mVoltage.setText(batteryVoltage); + mTemperature.setText(beaconTemperature); + mPduCount.setText(pduCount); + mTimeSinceReboot.setText(timeSinceBoot); + } + break; + + case TYPE_EID: + mFrameTypeView.setText(getString(R.string.type_eid)); + final String timerExponent = intent.getExtras().getString(UpdateService.EXTRA_TIMER_EXPONENT); + final String clockValue = intent.getExtras().getString(UpdateService.EXTRA_CLOCK_VALUE); + final String eid = intent.getExtras().getString(UpdateService.EXTRA_EID, ""); + mRwAdvertisingSlot = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + + mEidDataContainer.setVisibility(View.VISIBLE); + mUrlDataContainer.setVisibility(View.GONE); + mUidDataContainer.setVisibility(View.GONE); + mTlmDataContainer.setVisibility(View.GONE); + mEtlmDataContainer.setVisibility(View.GONE); + mTimerExponent.setText(timerExponent); + mClockValue.setText(clockValue); + mEid.setText(eid); + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + mProgressDialogHandler.removeCallbacks(mRunnableHandler); + } + break; + case EMPTY_SLOT: + mFrameTypeView.setText(getString(R.string.slot_state_empty)); + mRwAdvertisingSlot = intent.getByteArrayExtra(UpdateService.EXTRA_DATA); + mUrlDataContainer.setVisibility(View.GONE); + mUidDataContainer.setVisibility(View.GONE); + mTlmDataContainer.setVisibility(View.GONE); + mEidDataContainer.setVisibility(View.GONE); + break; + } + } + + private void updateUiWithFrameType(final byte[] readWriteAdvSlot) { + switch (mFrameType) { + case TYPE_UID: + mFrameTypeView.setText(getString(R.string.type_uid)); + final String namespaceId = ParserUtils.bytesToHex(readWriteAdvSlot, 2, 10, true); + final String instanceId = ParserUtils.bytesToHex(readWriteAdvSlot, 12, 6, true); + mUidDataContainer.setVisibility(View.VISIBLE); + mUrlDataContainer.setVisibility(View.GONE); + mTlmDataContainer.setVisibility(View.GONE); + mEtlmDataContainer.setVisibility(View.GONE); + mEidDataContainer.setVisibility(View.GONE); + mNamespaceId.setText(namespaceId); + mInstanceId.setText(instanceId); + break; + + case TYPE_URL: + mFrameTypeView.setText(getString(R.string.type_url)); + final String url = ParserUtils.decodeUri(readWriteAdvSlot, 2, readWriteAdvSlot.length - 2); + mUrlDataContainer.setVisibility(View.VISIBLE); + mUidDataContainer.setVisibility(View.GONE); + mTlmDataContainer.setVisibility(View.GONE); + mEtlmDataContainer.setVisibility(View.GONE); + mEidDataContainer.setVisibility(View.GONE); + + final SpannableString urlAttachment = new SpannableString(url); + urlAttachment.setSpan(new UnderlineSpan(), 0, url.length(), 0); + mUrl.setText(urlAttachment); + //mUrl.setText(url); + break; + + case TYPE_TLM: + mEidDataContainer.setVisibility(View.GONE); + mUrlDataContainer.setVisibility(View.GONE); + mUidDataContainer.setVisibility(View.GONE); + final boolean containsEid = mActiveSlotsTypes.contains(getString(R.string.type_eid)); + if(containsEid){ + mEtlmDataContainer.setVisibility(View.VISIBLE); + mFrameTypeView.setText(getString(R.string.type_etlm)); + final String etlm = ParserUtils.bytesToHex(readWriteAdvSlot, 2, 12, true); + final String salt = ParserUtils.bytesToHex(readWriteAdvSlot, 14, 2, true); + final String messageIntegrityCheck = ParserUtils.bytesToHex(readWriteAdvSlot, 16, 2, true); + + mEtlm.setText(etlm); + mEtlmSalt.setText(salt); + mEtlmMessageIntCheck.setText(messageIntegrityCheck); + + + } else { + mFrameTypeView.setText(getString(R.string.type_tlm)); + mTlmDataContainer.setVisibility(View.VISIBLE); + final int voltage = ParserUtils.decodeUint16BigEndian(readWriteAdvSlot, 2); + if (voltage > 0) { + mVoltage.setText(String.valueOf(voltage)); + } else { + mVoltage.setText(getString(R.string.batt_voltage_unsupported)); + } + + final float temp = ParserUtils.decode88FixedPointNotation(readWriteAdvSlot, 4); + if (temp > -128.0f) + mTemperature.setText(String.valueOf(temp)); + else + mTemperature.setText(getString(R.string.temperature_unsupported)); + + final String pduCount = String.valueOf(ParserUtils.decodeUint32BigEndian(readWriteAdvSlot, 6)); + final String timeSinceBoot = String.valueOf(ParserUtils.decodeUint32BigEndian(readWriteAdvSlot, 10) * 100); + + mPduCount.setText(pduCount); + mTimeSinceReboot.setText(timeSinceBoot); + } + + break; + + case TYPE_EID: + mFrameTypeView.setText(getString(R.string.type_eid)); + final String timerExponent = String.valueOf(ParserUtils.getIntValue(readWriteAdvSlot, 1, BluetoothGattCharacteristic.FORMAT_UINT8)); + final String clockValue = String.valueOf(ParserUtils.getIntValue(readWriteAdvSlot, 2, ParserUtils.FORMAT_UINT32_BIG_INDIAN)); + final String eid = String.valueOf(ParserUtils.bytesToHex(readWriteAdvSlot, 6, 8, true)); + mEidDataContainer.setVisibility(View.VISIBLE); + mUrlDataContainer.setVisibility(View.GONE); + mUidDataContainer.setVisibility(View.GONE); + mTlmDataContainer.setVisibility(View.GONE); + mEtlmDataContainer.setVisibility(View.GONE); + mTimerExponent.setText(timerExponent); + mClockValue.setText(clockValue); + mEid.setText(eid); + break; + case EMPTY_SLOT: + mFrameTypeView.setText(getString(R.string.slot_state_empty)); + mUrlDataContainer.setVisibility(View.GONE); + mUidDataContainer.setVisibility(View.GONE); + mTlmDataContainer.setVisibility(View.GONE); + mEtlmDataContainer.setVisibility(View.GONE); + mEidDataContainer.setVisibility(View.GONE); + break; + } + } + + private void updateActiveSlotSpinner(int maxSupportedSlots) { + mMaxSupportedSlots = new ArrayList<>(); + + for (int i = 0; i < maxSupportedSlots; i++) { + mMaxSupportedSlots.add("Slot " + i); + } + + mMaxActiveSlotsAdapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_item, mMaxSupportedSlots); + mMaxActiveSlotsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mIsActiveSlotAdapterUpdated = true; + mActiveSlots.setAdapter(mMaxActiveSlotsAdapter); + } + + @Override + public void onStart() { + super.onStart(); + + // This will connect to the service only if it's already running + final Activity activity = getActivity(); + final Intent service = new Intent(activity, UpdateService.class); + mBounnd = activity.bindService(service, mServiceConnection, 0); + } + + @Override + public void onStop() { + super.onStop(); + + if (mBounnd) + getActivity().unbindService(mServiceConnection); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (getActivity().isFinishing()) { + final Activity activity = getActivity(); + final Intent service = new Intent(activity, UpdateService.class); + activity.stopService(service); + } + + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + } + + final MainActivity parent = (MainActivity) mContext; + parent.setBeaconsFragment(null); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mContext = context; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final MainActivity parent = (MainActivity) mContext; + parent.setUPdateFragment(this); + } + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.fragment_update, container, false); + mProgressDialogHandler = new Handler(); + mBeaconConfigurationContainer = (LinearLayout) view.findViewById(R.id.beacon_configuration_container); + mFrameTypeContainer = (LinearLayout) view.findViewById(R.id.frame_type_container); + mFrameTypeView = (TextView) view.findViewById(R.id.frame_type); + mFrameTypeContainer = (LinearLayout) view.findViewById(R.id.frame_type_container); + mUidDataContainer = (LinearLayout) view.findViewById(R.id.uid_data_container); + mUrlDataContainer = (LinearLayout) view.findViewById(R.id.url_data_container); + mTlmDataContainer = (LinearLayout) view.findViewById(R.id.tlm_data_container); + mEtlmDataContainer= (LinearLayout) view.findViewById(R.id.etlm_data_container); + mEidDataContainer = (LinearLayout) view.findViewById(R.id.eid_data_container); + mEditSlot = (ImageView) view.findViewById(R.id.edit_slot); + mEditAdvInterval = (ImageView) view.findViewById(R.id.edit_adv_interval); + mShowBroadcastCapabilities = (ImageView) view.findViewById(R.id.show_broadcast_capabilities); + mShowSlotInfo = (ImageView) view.findViewById(R.id.show_slot_info); + mEditRadioTxPower = (ImageView) view.findViewById(R.id.edit_radio_tx_power); + mActiveSlots = (Spinner) view.findViewById(R.id.active_slots); + mNamespaceId = (TextView) view.findViewById(R.id.namespace_id); + mInstanceId = (TextView) view.findViewById(R.id.instance_id); + mUrl = (TextView) view.findViewById(R.id.url_data); + mEtlm = (TextView) view.findViewById(R.id.etlm_data); + mEtlmSalt = (TextView) view.findViewById(R.id.etlm_salt); + mEtlmMessageIntCheck = (TextView) view.findViewById(R.id.etlm_message_integrity_check); + mVoltage = (TextView) view.findViewById(R.id.voltage); + mTemperature = (TextView) view.findViewById(R.id.temperature); + mPduCount = (TextView) view.findViewById(R.id.advertiser_count); + mTimeSinceReboot = (TextView) view.findViewById(R.id.time_since_boot); + mTimerExponent = (TextView) view.findViewById(R.id.timer_exponent); + mClockValue = (TextView) view.findViewById(R.id.clock_value); + mEid = (TextView) view.findViewById(R.id.eid); + mAdvertisingInterval = (TextView) view.findViewById(R.id.adv_interval_ms); + mRadioTxPower = (TextView) view.findViewById(R.id.radio_tx_power); + mBeaconHelp = (TextView) view.findViewById(R.id.beacon_update_help); + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setCancelable(false); + mProgressDialog.setCanceledOnTouchOutside(false); + + initDataFromSharedPrefs(); + // Configure the CONNECT / DISCONNECT button + + final Button connectButton = mConnectButton = (Button) view.findViewById(R.id.action_connect); + connectButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + + if (mBinder == null) { + if(isBleEnabled()) { + if(isLocationEnabled()) { + final ScannerFragment scannerFragment = ScannerFragment.getInstance(EDDYSTONE_GATT_CONFIG_SERVICE_UUID); + scannerFragment.show(getChildFragmentManager(), null); + } else { + showToast("Please enable location services to scan for devices"); + } + } else { + enableBle(); + } + } else { + mBinder.disconnectAndClose(); + updateUiForBeacons(BluetoothProfile.STATE_DISCONNECTED, UpdateService.LOCKED); + + } + } + }); + + mShowBroadcastCapabilities.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final BroadcastCapabilitesDialogFragment broadcastCapabilitesDialogFragment = BroadcastCapabilitesDialogFragment.newInstance(mBroadcastCapabilities); + broadcastCapabilitesDialogFragment.show(getChildFragmentManager(), null); + } + }); + + mShowSlotInfo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final AllSlotInfoDialogFragment m = AllSlotInfoDialogFragment.newInstance(mActiveSlotsTypes); + m.show(getChildFragmentManager(), null); + } + }); + + mEditSlot.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final ReadWriteAdvertisementSlotDialogFragment dialogFragment = ReadWriteAdvertisementSlotDialogFragment.newInstance(false, mActiveSlot, mRwAdvertisingSlot); + dialogFragment.show(getChildFragmentManager(), null); + } + }); + + mEditRadioTxPower.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final RadioTxPowerDialogFragment dialogFragment = RadioTxPowerDialogFragment.newInstance(ParserUtils.parse(mRadioTxPowerData, 0, mRadioTxPowerData.length, "").trim(), false); + dialogFragment.show(getChildFragmentManager(), null); + } + }); + + mEditAdvInterval.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final AdvertisingIntervalDialogFragment dialogFragment = AdvertisingIntervalDialogFragment.newInstance(mAdvertisingInterval.getText().toString().trim()); + dialogFragment.show(getChildFragmentManager(), null); + } + }); + + mActiveSlots.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + final byte[] data = new byte[1]; + data[0] = (byte) position; + if (mIsActiveSlotAdapterUpdated) + mIsActiveSlotAdapterUpdated = false; + else { + mEikGenerated = false; + mBinder.changeToSelectedActiveSlot(data); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + mUrl.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl.getText().toString())); + getActivity().startActivity(browserIntent); + } + }); + + return view; + } + + private boolean isVariableAdvertisingSupported() { + final int capabilities = ParserUtils.getIntValue(mBroadcastCapabilities, 3, BluetoothGattCharacteristic.FORMAT_UINT8); + + return (capabilities & IS_VARIABLE_TX_POWER_SUPPORTED) > 0; + } + + private void initDataFromSharedPrefs() { + final SharedPreferences sharedPreferences = getActivity().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + final String serviceEcdhKey = sharedPreferences.getString(SERVICE_ECDH_KEY, ""); + if (!serviceEcdhKey.isEmpty()) { + mServiceEcdhKey = new byte[32]; + ParserUtils.setByteArrayValue(mServiceEcdhKey, 0, serviceEcdhKey); + } + } + + @Override + public void onViewCreated(final View view, final Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + setHasOptionsMenu(false); + + LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mServiceBroadcastReceiver, createIntentFilters()); + } + + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + inflater.inflate(R.menu.menu_update, menu); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + final int id = item.getItemId(); + RegisterBeaconDialogFragment registerBeaconDialogFragment; + switch (id) { + case R.id.action_about: + /*final BoardHelpFragment helpFragment = BoardHelpFragment.getInstance(BoardHelpFragment.MODE_UPDATE); + helpFragment.show(getChildFragmentManager(), null);*/ + return true; + case R.id.action_clear_slot: + ClearSlotDialogFragment clearSlotDialogFragment = ClearSlotDialogFragment.newInstance(); + clearSlotDialogFragment.show(getChildFragmentManager(), null); + return true; + case R.id.action_refresh_slot: + if (mBinder != null) { + if (mProgressDialog != null) { + mProgressDialog.setTitle(getString(R.string.prog_dialog_reading)); + mProgressDialog.setMessage(getString(R.string.prog_dialog_rw_adv_slot_msg)); + mProgressDialog.show(); + mProgressDialogHandler.postDelayed(mRunnableHandler, 10000); + } + mBinder.startReadingCharacteristicsForActiveSlot(); + } + return true; + case R.id.action_lock: + if (!mIsBeaconLocked) { + LockStateDialogFragment lockStateDialogFragment = LockStateDialogFragment.newInstance(mCurrentLockState, mUnlockCode); + lockStateDialogFragment.show(getChildFragmentManager(), null); + } else { + Toast.makeText(getActivity(), getString(R.string.unlock_beacon_error), Toast.LENGTH_LONG).show(); + } + return true; + case R.id.action_ecdh_info: + if (mBeaconPublicEcdhKey != null) { + EcdhKeyInfoDialogFragment ecdhKeyInfoDialogFragment = EcdhKeyInfoDialogFragment.newInstance(mActiveSlot, mFrameType, + ParserUtils.bytesToHex(mBeaconPublicEcdhKey, 0, 32, true), ParserUtils.bytesToHex(mEncryptedIdentityKey, 0, 16, true), + ParserUtils.bytesToHex(mDecryptedIdentityKey, 0, 16, true)); + ecdhKeyInfoDialogFragment.show(getChildFragmentManager(), null); + } else { + ErrorDialogFragment errorDialogFragment = ErrorDialogFragment.newInstance(getString(R.string.error_no_ecdh_info)); + errorDialogFragment.show(getChildFragmentManager(), null); + } + break; + case R.id.action_register_beacons: + if(mFrameTypeView.getText().toString().equals("EMPTY")){ + return true; + } + switch (mFrameType) { + case TYPE_UID: + registerUidBeacon(); + return true; + case TYPE_EID: + registerBeaconDialogFragment = RegisterBeaconDialogFragment.newInstance(); + registerBeaconDialogFragment.show(getChildFragmentManager(), null); + //registerEidBeacon(); + return true; + default: + final String message = "Cannot register URL/TLM/eTLM beacon type"; + showToast(message); + return true; + + } + + case R.id.action_adv_advertised_tx_power: + RadioTxPowerDialogFragment dialogFragment = RadioTxPowerDialogFragment.newInstance(String.valueOf(mAdvancedAdvTxPower), true); + dialogFragment.show(getChildFragmentManager(), null); + break; + case R.id.action_remain_connectable: + RemainConnectableDialogFragment remainConnectableDialogFragment = RemainConnectableDialogFragment.newInstance(mRemainConnectable); + remainConnectableDialogFragment.show(getChildFragmentManager(), null); + break; + } + return false; + } + + private IntentFilter createIntentFilters() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(UpdateService.ACTION_STATE_CHANGED); + filter.addAction(UpdateService.ACTION_DONE); + filter.addAction(UpdateService.ACTION_GATT_ERROR); + filter.addAction(UpdateService.ACTION_UNLOCK_BEACON); + filter.addAction(UpdateService.ACTION_BROADCAST_CAPABILITIES); + filter.addAction(UpdateService.ACTION_ACTIVE_SLOT); + filter.addAction(UpdateService.ACTION_ADVERTISING_INTERVAL); + filter.addAction(UpdateService.ACTION_RADIO_TX_POWER); + filter.addAction(UpdateService.ACTION_ADVANCED_ADVERTISED_TX_POWER); + filter.addAction(UpdateService.ACTION_LOCK_STATE); + filter.addAction(UpdateService.ACTION_UNLOCK); + filter.addAction(UpdateService.ACTION_ECDH_KEY); + filter.addAction(UpdateService.ACTION_EID_IDENTITY_KEY); + filter.addAction(UpdateService.ACTION_READ_WRITE_ADV_SLOT); + filter.addAction(UpdateService.ACTION_ADVANCED_FACTORY_RESET); + filter.addAction(UpdateService.ACTION_ADVANCED_REMAIN_CONNECTABLE); + filter.addAction(UpdateService.ACTION_BROADCAST_ALL_SLOT_INFO); + Log.v(TAG, "Intent filters created"); + return filter; + } + + private void registerEidBeacon(final byte[] uid) { + final JSONObject jBody = createEidBeaconJson(uid); + if (jBody != null) { + if (mProximityApiClient != null) { + if (NetworkUtils.checkNetworkConnectivity(getActivity())) { + if (mProgressDialog != null) { + mProgressDialog.setTitle("Registering EID Beacon"); + mProgressDialog.setMessage("Please wait while the EID beacon is registered in the proximity API"); + mProgressDialog.show(); + mProgressDialogHandler.postDelayed(mRunnableHandler, 10000); + } + mProximityApiClient.registerBeacon(beaconRegistrationCallback, jBody); + } else showToast(getString(R.string.check_internet_connectivity)); + } else { + ensurePermission(new String[]{Manifest.permission.GET_ACCOUNTS}); + } + } else showToast(getString(R.string.service_ecdh_missing)); + } + + private void registerUidBeacon() { + final JSONObject jBody = createUidBeaconJson(); + if (jBody != null) + if (mProximityApiClient != null) { + if (NetworkUtils.checkNetworkConnectivity(getActivity())) { + authoriseAccount(getUserAccount()); + if (mProgressDialog != null) { + mProgressDialog.setTitle("Registering UID Beacon"); + mProgressDialog.setMessage("Please wait while the EID beacon is registered in the proximity API"); + mProgressDialog.show(); + mProgressDialogHandler.postDelayed(mRunnableHandler, 10000); + } + mProximityApiClient.registerBeacon(beaconRegistrationCallback, jBody); + } else showToast(getString(R.string.check_internet_connectivity)); + } else { + ensurePermission(new String[]{Manifest.permission.GET_ACCOUNTS}); + } + } + + private JSONObject createEidBeaconJson(final byte[] uid) { + + JSONObject body; + try { + body = new JSONObject(); + + JSONObject advertisedId = new JSONObject() + .put("type", "EDDYSTONE") + .put("id", ParserUtils.base64Encode(uid)); + body.put("advertisedId", advertisedId); + + body.put("status", "ACTIVE"); + + // TODO: encode the remaining beacon parameters like location, description, etc. + // https://developers.google.com/beacons/proximity/reference/rest/v1beta1/beacons#Beacon + if (mServiceEcdhKey == null) { + return null; + } + String beaconEcdhPublicKey = ParserUtils.base64Encode(mBeaconPublicEcdhKey); + String serviceEcdhPublicKey = ParserUtils.base64Encode(mServiceEcdhKey); + + final byte[] eidr = new byte[8]; + ParserUtils.setByteArrayValue(eidr, 0, mEid.getText().toString().trim().replace("0x", "")); + String initialEidr = ParserUtils.base64Encode(eidr); + + JSONObject ephemeralIdRegistration = new JSONObject() + .put("beaconEcdhPublicKey", beaconEcdhPublicKey) + .put("serviceEcdhPublicKey", serviceEcdhPublicKey) + .put("rotationPeriodExponent", mTimerExponent.getText().toString()) + .put("initialClockValue", mClockValue.getText().toString()) + .put("initialEid", initialEidr); + body.put("ephemeralIdRegistration", ephemeralIdRegistration); + + Log.d(TAG, "request:" + body.toString(2)); + } catch (JSONException e) { + //logAndShowToast("JSONException building request body", e); + return null; + } + return body; + } + + private JSONObject createUidBeaconJson() { + + JSONObject body; + try { + body = new JSONObject(); + String namespaceId = mNamespaceId.getText().toString().trim(); + String instanceId = mInstanceId.getText().toString().trim(); + if (namespaceId.startsWith("0x")) + namespaceId = namespaceId.substring(2, namespaceId.length()); + + if (instanceId.startsWith("0x")) + instanceId = instanceId.substring(2, instanceId.length()); + + final String uid = namespaceId + instanceId; + + JSONObject advertisedId = new JSONObject() + .put("type", "EDDYSTONE") + .put("id", ParserUtils.base64Encode(uid)); + body.put("advertisedId", advertisedId); + + body.put("status", "ACTIVE"); + + Log.d(TAG, "request:" + body.toString(2)); + } catch (JSONException e) { + //logAndShowToast("JSONException building request body", e); + return null; + } + return body; + } + + Callback beaconRegistrationCallback = new Callback() { + @Override + public void onFailure(Request request, IOException e) { + + } + + @Override + public void onResponse(Response response) throws IOException { + + try { + mProgressDialog.dismiss(); + mProgressDialogHandler.removeCallbacks(mRunnableHandler); + String body = response.body().string(); + final JSONObject jsonResponse = new JSONObject(body); + if (!response.isSuccessful()) { + final JSONObject jsonError = jsonResponse.getJSONObject("error"); + final int errorCode = jsonError.getInt("code"); + final String message = jsonError.getString("message"); + ProximityApiErrorDialogFragment errorDialogFragment; + switch (errorCode) { + case ERROR_ALREADY_EXISTS: + errorDialogFragment = ProximityApiErrorDialogFragment.newInstance(String.valueOf(errorCode), message, getString(R.string.error_beacon_already_exists)); + errorDialogFragment.show(getChildFragmentManager(), null); + break; + case ERROR_UNAUTHORIZED: + errorDialogFragment = ProximityApiErrorDialogFragment.newInstance(String.valueOf(errorCode), message, "Unable to register beacon"); + errorDialogFragment.show(getChildFragmentManager(), null); + Account userAccount = getUserAccount(); + if(userAccount != null) + new AuthorizedServiceTask(getActivity(), userAccount, AUTH_SCOPE_PROXIMITY_API).execute(); + break; + + case 403: + default: + errorDialogFragment = ProximityApiErrorDialogFragment.newInstance(String.valueOf(errorCode), "Unknown error", "Unable to register beacon"); + errorDialogFragment.show(getChildFragmentManager(), null); + break; + } + return; + } + showToast(getString(R.string.registration_success)); + + final String beaconName = jsonResponse.getString("beaconName"); + CreateAttachmentDialogFragment createAttachmentDialogFragment = CreateAttachmentDialogFragment.newInstance(beaconName); + createAttachmentDialogFragment.show(getChildFragmentManager(), null); + + } catch (JSONException e) { + e.printStackTrace(); + } + } + }; + + private void showToast(final String message) { + Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mServiceBroadcastReceiver); + Log.v(TAG, "Receiver unregistered!"); + mServiceBroadcastReceiver = null; + } + + @Override + public void onDeviceSelected(final BluetoothDevice device, final String name) { + if (mProgressDialog != null) { + mProgressDialog.setTitle(getString(R.string.prog_dialog_connect_title)); + mProgressDialog.setMessage(getString(R.string.prog_dialog_connect_message)); + mProgressDialog.show(); + } + + final Activity activity = getActivity(); + final Intent service = new Intent(activity, UpdateService.class); + service.putExtra(UpdateService.EXTRA_DATA, device); + updateUiForBeacons(BluetoothProfile.STATE_CONNECTED, UpdateService.LOCKED); + activity.startService(service); + mBounnd = true; + activity.bindService(service, mServiceConnection, 0); + } + + private final Runnable mRunnableHandler = new Runnable() { + @Override + public void run() { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + } + } + }; + + private void updateUiForBeacons(int connectionState, int lockState) { + switch (connectionState) { + case BluetoothProfile.STATE_CONNECTED: + if (UpdateService.LOCKED == lockState) { + mIsBeaconLocked = true; + mCurrentLockState = LOCKED; + } else if (UpdateService.UNLOCKED == lockState) { + mIsBeaconLocked = false; + mCurrentLockState = UNLOCKED; + if (mUnlockBeaconDialogFragment != null) mUnlockBeaconDialogFragment.dismiss(); + mBeaconHelp.setVisibility(View.GONE); + mBeaconConfigurationContainer.setVisibility(View.VISIBLE); + mFrameTypeContainer.setVisibility(View.VISIBLE); + } else if (UpdateService.UNLOCKED_AUTOMATIC_RELOCK_DISABLED == lockState) { + mCurrentLockState = UNLOCKED_AUTOMATIC_RELOCK_DISABLED; + mIsBeaconLocked = false; + if (mUnlockBeaconDialogFragment != null) mUnlockBeaconDialogFragment.dismiss(); + mBeaconHelp.setVisibility(View.GONE); + mBeaconConfigurationContainer.setVisibility(View.VISIBLE); + mFrameTypeContainer.setVisibility(View.VISIBLE); + } + break; + case BluetoothProfile.STATE_DISCONNECTED: + setHasOptionsMenu(false); + getActivity().invalidateOptionsMenu(); + mBeaconHelp.setVisibility(View.VISIBLE); + mBeaconConfigurationContainer.setVisibility(View.GONE); + mFrameTypeContainer.setVisibility(View.GONE); + //clear all resources on disconnection + mBeaconPublicEcdhKey = null; + mDecryptedIdentityKey = null; + if (mServiceEcdhKey != null) { + final SharedPreferences sharedPreferences = getActivity().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(SERVICE_ECDH_KEY, ParserUtils.bytesToHex(mServiceEcdhKey, 0, 32, false)).apply(); + } + mServiceEcdhKey = null; + mBeaconEcdhPrivateKey = null; + mIsBeaconLocked = true; + break; + } + } + + @Override + public void unlockBeacon(byte[] encryptedLockCode, final byte[] beaconLockCode) { + setHasOptionsMenu(true); + getActivity().invalidateOptionsMenu(); + mUnlockCode = beaconLockCode; + mBinder.unlockBeacon(encryptedLockCode, beaconLockCode); + } + + @Override + public void cancelUnlockBeacon() { + final Activity activity = getActivity(); + final Intent service = new Intent(activity, UpdateService.class); + activity.stopService(service); + } + + @Override + public void configureUidSlot(byte[] uidSlotData) { + mActiveSlotsTypes.set(mActiveSlot, "UID"); + mBinder.configureActiveSlot(uidSlotData, "UID"); + } + + @Override + public void configureUrlSlot(byte[] urlSlotData) { + mActiveSlotsTypes.set(mActiveSlot, "URL"); + mBinder.configureActiveSlot(urlSlotData, "URL"); + } + + @Override + public void configureTlmSlot(byte[] tlmSlotData) { + mActiveSlotsTypes.set(mActiveSlot, "TLM"); + mBinder.configureActiveSlot(tlmSlotData, "TLM"); + } + + @Override + public void configureEidSlot(final byte[] eidSlotData) { + + if (mProximityApiClient != null) { + if (NetworkUtils.checkNetworkConnectivity(getActivity())) { + if (mProgressDialog != null) { + mProgressDialog.setTitle(getString(R.string.prog_dialog_config_eid_title)); + mProgressDialog.setMessage(getString(R.string.retrieve_resolver_keys)); + mProgressDialog.show(); + mProgressDialogHandler.postDelayed(mRunnableHandler, 10000); + } + mProximityApiClient.getEphemeralIdRegistrationParams(new Callback() { + @Override + public void onFailure(Request request, IOException e) { + + } + + @Override + public void onResponse(Response response) throws IOException { + + try { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + mProgressDialogHandler.removeCallbacks(mRunnableHandler); + } + JSONObject jsonResponse = new JSONObject(response.body().string()); + if (!response.isSuccessful()) { + final JSONObject jsonError = jsonResponse.getJSONObject("error"); + final int errorCode = jsonError.getInt("code"); + final String message = jsonError.getString("message"); + ProximityApiErrorDialogFragment errorDialogFragment; + switch (errorCode) { + case ERROR_UNAUTHORIZED: + Account userAccount = getUserAccount(); + if (userAccount != null) + new AuthorizedServiceTask(getActivity(), userAccount, AUTH_SCOPE_PROXIMITY_API).execute(); + return; + default: + errorDialogFragment = ProximityApiErrorDialogFragment.newInstance(String.valueOf(errorCode), message, ""); + errorDialogFragment.show(getChildFragmentManager(), null); + return; + } + } + String body = response.body().string(); + //final JSONObject json = new JSONObject(body); + Log.d(TAG, "getEphemeralIdRegistrationParams response: " + jsonResponse.toString(2)); + String serviceEcdhPublicKey = jsonResponse.getString("serviceEcdhPublicKey"); + mServiceEcdhKey = ParserUtils.base64Decode(serviceEcdhPublicKey); + Log.d(TAG, "Service ECDH Key: " + ParserUtils.bytesToHex(mServiceEcdhKey, 0, 32, false)); + + if (mProgressDialog != null && mProgressDialog.isShowing()) + mProgressDialog.setMessage(getString(R.string.writing_rw_adv_slot_char)); + + if (eidSlotData.length == 34) { + mEikGenerated = false; + System.arraycopy(mServiceEcdhKey, 0, eidSlotData, 1, mServiceEcdhKey.length); + mActiveSlotsTypes.set(mActiveSlot, "EID"); + mBinder.configureActiveSlot(eidSlotData, "EID"); + } else { + byte[] beaconPrivateEcdhKey = new byte[32]; + new Random().nextBytes(beaconPrivateEcdhKey); + Log.d(TAG, "Beacon ECDH Private Key: " + ParserUtils.bytesToHex(beaconPrivateEcdhKey, 0, 32, false)); + mBeaconEcdhPrivateKey = ParserUtils.bytesToHex(beaconPrivateEcdhKey, 0, 32, false); + generator = new EddystoneEidrGenerator(mServiceEcdhKey, beaconPrivateEcdhKey); + mBeaconPublicEcdhKey = generator.getBeaconPublicKey(); + Log.d(TAG, "Beacon ECDH Public Key: " + ParserUtils.bytesToHex(mBeaconPublicEcdhKey, 0, 32, false)); + byte[] identityKey = generator.getIdentityKey(); + mEikGenerated = true; + Log.d(TAG, "Unencrypted Idenity Key: " + ParserUtils.bytesToHex(identityKey, 0, 16, false)); + Log.v(TAG, "Encrypted Identity key: " + ParserUtils.bytesToHex(identityKey, 0, 16, true)); + identityKey = ParserUtils.aes128Encrypt(identityKey, new SecretKeySpec(mUnlockCode, "AES")); + System.arraycopy(identityKey, 0, eidSlotData, 1, identityKey.length); + mActiveSlotsTypes.set(mActiveSlot, "EID"); + mBinder.configureActiveSlot(eidSlotData, "EID"); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + }); + } else showToast(getString(R.string.check_internet_connectivity)); + } else { + ensurePermission(new String[]{Manifest.permission.GET_ACCOUNTS}); + showToast(getString(R.string.reconnect_to_proximity)); + } + } + + @Override + public void lockBeacon(byte[] lockCode) { + if (lockCode.length == 17) + System.arraycopy(lockCode, 1, mUnlockCode, 0, 16); + mBinder.lockBeacon(lockCode); + } + + public byte[] aes128decrypt(byte[] data, SecretKeySpec keySpec) { + Cipher cipher; + try { + // Ignore the "ECB encryption should not be used" warning. We use exactly one block so + // the difference between ECB and CBC is just an IV or not. In addition our blocks are + // always different since they have a monotonic timestamp. Most importantly, our blocks + // aren't sensitive. Decrypting them means means knowing the beacon time and its rotation + // period. If due to ECB an attacker could find out that the beacon broadcast the same + // block a second time, all it could infer is that for some reason the clock of the beacon + // reset, which is not very helpful + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + Log.e(TAG, "Error constructing cipher instance", e); + return null; + } + + try { + cipher.init(Cipher.DECRYPT_MODE, keySpec); + } catch (InvalidKeyException e) { + Log.e(TAG, "Error initializing cipher instance", e); + return null; + } + + byte[] ret; + try { + ret = cipher.doFinal(data); + } catch (IllegalBlockSizeException | BadPaddingException e) { + Log.e(TAG, "Error executing cipher", e); + return null; + } + + return ret; + } + + @Override + public void registerBeaconListener(byte[] uid) { + mUidForEid = uid; + registerEidBeacon(uid); + } + + @Override + public void createAttachmentForBeacon(final String mBeaconName, final byte[] attachmentData) { + if (mProximityApiClient != null) { + mProgressDialog.show(); + mProgressDialogHandler.postDelayed(mRunnableHandler, 10000); + mProximityApiClient.createAttachment(mCreateAttachmentCallback, mBeaconName, createBeaconAttachment(attachmentData)); + } else { + ensurePermission(new String[] {Manifest.permission.GET_ACCOUNTS}); + } + } + + private final Callback mCreateAttachmentCallback = new Callback() { + @Override + public void onFailure(Request request, IOException e) { + Log.v(TAG, "Beacon attachmentm: " + request.toString()); + } + + @Override + public void onResponse(Response response) throws IOException { + try { + mProgressDialog.dismiss(); + mProgressDialogHandler.removeCallbacks(mRunnableHandler); + String body = response.body().string(); + final JSONObject jsonResponse = new JSONObject(body); + if (!response.isSuccessful()){ + final JSONObject jsonError = jsonResponse.getJSONObject("error"); + final int errorCode = jsonError.getInt("code"); + final String message = jsonError.getString("message"); + ProximityApiErrorDialogFragment errorDialogFragment; + switch (errorCode) { + case ERROR_UNAUTHORIZED: + Account userAccount = getUserAccount(); + if(userAccount != null) + new AuthorizedServiceTask(getActivity(), userAccount, AUTH_SCOPE_PROXIMITY_API).execute(); + return; + default: + errorDialogFragment = ProximityApiErrorDialogFragment.newInstance(String.valueOf(errorCode), message, ""); + errorDialogFragment.show(getChildFragmentManager(), null); + return; + } + } + showToast(getString(R.string.attachment_siccess)); + + Log.v(TAG, "Beacon attachmentm: " + response.toString()); + } catch (JSONException e) { + e.printStackTrace(); + } + } + }; + + private JSONObject createBeaconAttachment(final byte[] attachment) { + + JSONObject body; + try { + body = new JSONObject() + .put("namespacedType", APP_NAMESPACE_TYPE) + .put("data", ParserUtils.base64Encode(attachment)); + Log.d(TAG, "request:" + body.toString(2)); + } catch (JSONException e) { + return null; + } + return body; + } + + @Override + public void configureRadioTxPower(final byte[] radioTxPower, final boolean advanceTxPowerSupported) { + if (!advanceTxPowerSupported) + mBinder.configureRadioTxPower(radioTxPower); + else + mBinder.configureAdvancedAdvertisedTxPower(radioTxPower); + } + + @Override + public void clearSlot() { + mBinder.configureActiveSlot(new byte[]{0x00}, "EMPTY"); //configuring active slot with a 0 byte will clear the slot + } + + @Override + public void configureAdvertisingInterval(final byte[] advertisingInterval) { + mBinder.configureAdvertistingInterval(advertisingInterval); + } + + private String getUserAccountName() { + SharedPreferences sharedPreferences = getActivity().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + return sharedPreferences.getString(ACCOUNT_NAME_PREF, null); + } + + private Account getUserAccount() { + final String name = getUserAccountName(); + if (!name.isEmpty()) { + final Account[] accounts = AccountManager.get(getActivity()).getAccounts(); + for (Account account : accounts) { + if (account.name.equals(name)) { + mAccountName = account.name; + return account; + } + } + } + return null; + } + + + + private void setUserAccountName(final String accountName){ + SharedPreferences sharedPreferences = getActivity().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(ACCOUNT_NAME_PREF, accountName).apply(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case REQUEST_PERMISSION_REQ_CODE: { + if(permissions.length > 0) + for(int i = 0; i < permissions.length; i++){ + if (permissions[i].equals(Manifest.permission.GET_ACCOUNTS)){ + if(grantResults[i] == PackageManager.PERMISSION_GRANTED) + onPermissionGranted(permissions[i]); + else Toast.makeText(getActivity(), R.string.rationale_permission_denied, Toast.LENGTH_SHORT).show(); + } else if (permissions[i].equals(Manifest.permission.ACCESS_COARSE_LOCATION)) { + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) + onPermissionGranted(permissions[i]); + else Toast.makeText(getActivity(), R.string.rationale_permission_denied, Toast.LENGTH_SHORT).show(); + } + } + break; + } + } + } + + @Override + protected void onPermissionGranted(String permission) { + Account account; + if (Manifest.permission.GET_ACCOUNTS.equalsIgnoreCase(permission)){ + String accountName = getUserAccountName(); + if (mProximityApiClient == null) { + if (accountName == null) { + String[] accountTypes = new String[]{"com.google"}; + Intent intent = AccountPicker.newChooseAccountIntent( + null, null, accountTypes, false, null, null, null, null); + startActivityForResult(intent, REQUEST_CODE_USER_ACCOUNT); + } else { + account = getUserAccount(); + if(account != null) { + authoriseAccount(account); + mProximityApiClient = new ProximityBeaconImpl(getActivity(), account); + } + } + } else { + account = getUserAccount(); + if(account != null) { + authoriseAccount(account); + if(mProximityApiClient == null) { + mProximityApiClient = new ProximityBeaconImpl(getActivity(), account); + } + } else Toast.makeText(getActivity(), getString(R.string.user_account_unavailable), Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode){ + case REQUEST_ENABLE_BT: + if(data != null) + if (resultCode != Activity.RESULT_OK) { + showToast(getString(R.string.rationale_permission_denied)); + onDestroy(); + } else { + if(ensurePermission(new String [] {Manifest.permission.ACCESS_COARSE_LOCATION})) { + if(isLocationEnabled()) { + final ScannerFragment scannerFragment = ScannerFragment.getInstance(EDDYSTONE_GATT_CONFIG_SERVICE_UUID); + scannerFragment.show(getChildFragmentManager(), null); + } else { + showToast("Please enable location services to scan for devices"); + } + } + } + break; + case REQUEST_CODE_USER_ACCOUNT: + if (resultCode == Activity.RESULT_OK) { + if (data != null) { + final String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); + setUserAccountName(accountName); + // The first time the account tries to contact the beacon service we'll pop a dialog + // asking the user to authorize our activity. Ensure that's handled cleanly here, rather + // than when the scan tries to fetch the status of every beacon within range. + Account account = getUserAccount(); + if(account != null) + authoriseAccount(account); + else showToast(getString(R.string.user_account_unavailable)); + } + } else { + showToast(getString(R.string.rationale_permission_denied)); + } + break; + } + } + + private void authoriseAccount(final Account account){ + if(NetworkUtils.checkNetworkConnectivity(getActivity())) { + new AuthorizedServiceTask(getActivity(), account, AUTH_SCOPE_PROXIMITY_API).execute(); + } + else + showToast(getString(R.string.check_internet_connectivity)); + } + + /** + * Checks whether the Bluetooth adapter is enabled. + */ + private boolean isBleEnabled() { + final BluetoothManager bm = (BluetoothManager) getActivity().getSystemService(Context.BLUETOOTH_SERVICE); + final BluetoothAdapter ba = bm.getAdapter(); + return ba != null && ba.isEnabled(); + } + + /** + * Tries to start Bluetooth adapter. + */ + private void enableBle() { + final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableIntent, REQUEST_ENABLE_BT); + } + + public boolean isLocationEnabled() { + if (checkIfVersionIsMarshmallowOrAbove()) { + int locationMode = Settings.Secure.LOCATION_MODE_OFF; + + try { + locationMode = Settings.Secure.getInt(getActivity().getContentResolver(), Settings.Secure.LOCATION_MODE); + } catch (final Settings.SettingNotFoundException e) { + // do nothing + } + return locationMode != Settings.Secure.LOCATION_MODE_OFF; + } + return true; + } + + private boolean checkIfVersionIsMarshmallowOrAbove() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/AuthTaskUrlShortener.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/AuthTaskUrlShortener.java new file mode 100644 index 0000000..bc98a40 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/AuthTaskUrlShortener.java @@ -0,0 +1,124 @@ +package no.nordicsemi.android.nrfbeacon.nearby.util; + +import android.accounts.Account; +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.util.Log; + +import com.google.android.gms.auth.GoogleAuthException; +import com.google.android.gms.auth.GoogleAuthUtil; +import com.google.android.gms.auth.GooglePlayServicesAvailabilityException; +import com.google.android.gms.auth.UserRecoverableAuthException; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +/** + * Created by rora on 07.04.2016. + */ +public class AuthTaskUrlShortener extends AsyncTask { + static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR = 1002; + public static final String AUTHORIZATION = "Authorization"; + public static final String BEARER = "Bearer "; + public static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); + private static final String TAG = "BEACON"; + private final OkHttpClient mOkHttpClient; + private final Callback mCallBack; + private final String longUrl; + private final String BASE_URL = "https://www.googleapis.com/urlshortener/v1/url";//?key=AIzaSyDlSUOly5_hQkzeyxOr5Ff5c8AuNWaUcZM"; + private static final String SCOPE = "oauth2:https://www.googleapis.com/auth/urlshortener"; + private static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_URL_SHORTNER = 1004; + private static final String AUTH_PROXIMITY_API = "oauth2:https://www.googleapis.com/auth/userlocation.beacon.registry"; + private static final String AUTH_SCOPE_URL_SHORTENER = "oauth2:https://www.googleapis.com/auth/urlshortener"; + private Activity mActivity; + private Account mAccount; + + public AuthTaskUrlShortener(final Callback mCallBack, final String longUrl, Activity context, Account account){ + this.mOkHttpClient = new OkHttpClient(); + this.mCallBack = mCallBack; + this.longUrl = longUrl; + this.mActivity = context; + this.mAccount = account; + } + + @Override + protected Void doInBackground(Void... params) { + final JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("longUrl", longUrl); + } catch (JSONException e) { + e.printStackTrace(); + } + final String token; + try { + token = GoogleAuthUtil.getToken(mActivity, mAccount, SCOPE); + Request.Builder requestBuilder = new Request.Builder() + .header(AUTHORIZATION, BEARER + token) + .url(BASE_URL) + .post(RequestBody.create(MEDIA_TYPE_JSON, jsonObject.toString())); + + Request request = requestBuilder.build(); + mOkHttpClient.newCall(request).enqueue(new HttpCallback(mCallBack)); + } catch (UserRecoverableAuthException e) { + // GooglePlayServices.apk is either old, disabled, or not present + // so we need to show the user some UI in the activity to recover. + handleAuthException(mActivity, e); + Log.e(TAG, "UserRecoverableAuthException", e); + } catch (GoogleAuthException e) { + // Some other type of unrecoverable exception has occurred. + // Report and log the error as appropriate for your app. + Log.e(TAG, "GoogleAuthException", e); + } catch (IOException e) { + // The fetchToken() method handles Google-specific exceptions, + // so this indicates something went wrong at a higher level. + // TIP: Check for network connectivity before starting the AsyncTask. + Log.e(TAG, "IOException", e); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + } + + private void handleAuthException(final Activity activity, final Exception e) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (e instanceof GooglePlayServicesAvailabilityException) { + // The Google Play services APK is old, disabled, or not present. + // Show a dialog created by Google Play services that allows + // the user to update the APK + int statusCode = ((GooglePlayServicesAvailabilityException) e).getConnectionStatusCode(); + Dialog dialog = GooglePlayServicesUtil.getErrorDialog( + statusCode, activity, REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_URL_SHORTNER); + dialog.show(); + } else if (e instanceof UserRecoverableAuthException) { + // Unable to authenticate, such as when the user has not yet granted + // the app access to the account, but the user can fix this. + // Forward the user to an activity in Google Play services. + Intent intent = ((UserRecoverableAuthException) e).getIntent(); + activity.startActivityForResult( + intent, REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR_FOR_URL_SHORTNER); + + } + } + }); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/DebugLogger.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/DebugLogger.java new file mode 100644 index 0000000..99d38a1 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/DebugLogger.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.util; + +import android.util.Log; + +import no.nordicsemi.android.nrfbeacon.nearby.BuildConfig; + +public class DebugLogger { + public static void v(final String tag, final String text) { + if (BuildConfig.DEBUG) + Log.v(tag, text); + } + + public static void d(String tag, String text) { + if (BuildConfig.DEBUG) + Log.d(tag, text); + } + + public static void i(final String tag, final String text) { + if (BuildConfig.DEBUG) + Log.i(tag, text); + } + + public static void w(String tag, String text) { + if (BuildConfig.DEBUG) + Log.w(tag, text); + } + + public static void e(final String tag, final String text) { + if (BuildConfig.DEBUG) + Log.e(tag, text); + } + + public static void wtf(String tag, String text) { + if (BuildConfig.DEBUG) + Log.wtf(tag, text); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/HttpCallback.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/HttpCallback.java new file mode 100644 index 0000000..dde2ef9 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/HttpCallback.java @@ -0,0 +1,61 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package no.nordicsemi.android.nrfbeacon.nearby.util; + +import android.os.Handler; +import android.os.Looper; + +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import java.io.IOException; + +/** + * A wrapper around OkHttp's Callback class that runs its methods on the UI thread. + */ +class HttpCallback implements Callback { + private final Callback delegate; + private final Handler handler; + + public HttpCallback(Callback delegate) { + this.delegate = delegate; + this.handler = new Handler(Looper.getMainLooper()); + } + + @Override + public void onFailure(final Request request, final IOException e) { + handler.post(new Runnable() { + @Override + public void run() { + delegate.onFailure(request, e); + } + }); + } + + @Override + public void onResponse(final Response response) throws IOException { + handler.post(new Runnable() { + @Override + public void run() { + try { + delegate.onResponse(response); + } catch (IOException e) { + delegate.onFailure(null, e); + } + } + }); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/NetworkUtils.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/NetworkUtils.java new file mode 100644 index 0000000..c33c537 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/NetworkUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrfbeacon.nearby.util; + +import android.app.Activity; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +/** + * Created by rora on 12.04.2016. + */ +public class NetworkUtils { + + public static boolean checkNetworkConnectivity(final Activity activity){ + ConnectivityManager cm = (ConnectivityManager)activity.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/ParserUtils.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/ParserUtils.java new file mode 100644 index 0000000..1a7983e --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/util/ParserUtils.java @@ -0,0 +1,755 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ +package no.nordicsemi.android.nrfbeacon.nearby.util; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.util.Base64; +import android.util.Log; +import android.util.SparseArray; +import android.util.Xml; +import android.webkit.URLUtil; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.Random; +import java.util.UUID; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +public class ParserUtils { + private static final char[] HEX_ARRAY = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + public static final int FORMAT_UINT24 = 0x13; + public static final int FORMAT_SINT24 = 0x23; + public static final int FORMAT_UINT16_BIG_INDIAN = 0x62; + public static final int FORMAT_UINT32_BIG_INDIAN = 0x64; + + + /** + * URI Scheme maps a byte code into the scheme and an optional scheme specific prefix. + */ + private static final SparseArray URI_SCHEMES = new SparseArray() { + { + put((byte) 0, "http://www."); + put((byte) 1, "https://www."); + put((byte) 2, "http://"); + put((byte) 3, "https://"); + put((byte) 4, "urn:uuid:"); // RFC 2141 and RFC 4122}; + } + }; + + /** + * Expansion strings for "http" and "https" schemes. These contain strings appearing anywhere in a + * URL. Restricted to Generic TLDs. + *

+ * Note: this is a scheme specific encoding. + */ + private static final SparseArray URL_CODES = new SparseArray() { + { + put((byte) 0, ".com/"); + put((byte) 1, ".org/"); + put((byte) 2, ".edu/"); + put((byte) 3, ".net/"); + put((byte) 4, ".info/"); + put((byte) 5, ".biz/"); + put((byte) 6, ".gov/"); + put((byte) 7, ".com"); + put((byte) 8, ".org"); + put((byte) 9, ".edu"); + put((byte) 10, ".net"); + put((byte) 11, ".info"); + put((byte) 12, ".biz"); + put((byte) 13, ".gov"); + } + }; + private static final String TAG = "MCP"; + + public static String bytesToHex(final byte[] bytes, final boolean add0x) { + if (bytes == null) + return ""; + return bytesToHex(bytes, 0, bytes.length, add0x); + } + + public static String bytesToHex(final byte[] bytes, final int start, final int length, final boolean add0x) { + if (bytes == null || bytes.length <= start || length <= 0) + return ""; + + final int maxLength = Math.min(length, bytes.length - start); + final char[] hexChars = new char[maxLength * 2]; + for (int j = 0; j < maxLength; j++) { + final int v = bytes[start + j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + if (!add0x) + return new String(hexChars); + return "0x" + new String(hexChars); + } + + public static String bytesToAddress(final byte[] bytes, final int start) { + if (bytes == null || bytes.length < start + 6) + return ""; + + final int maxLength = 6; + final char[] hexChars = new char[maxLength * 3 - 1]; + for (int j = 0; j < maxLength; j++) { + final int v = bytes[start + j] & 0xFF; + hexChars[j * 3] = HEX_ARRAY[v >>> 4]; + hexChars[j * 3 + 1] = HEX_ARRAY[v & 0x0F]; + if (j < maxLength - 1) + hexChars[j * 3 + 2] = ':'; + } + return new String(hexChars); + } + + public static UUID bytesToUUID(final byte[] bytes, final int start, final int length) { + if (bytes == null || bytes.length < start + 16 || length != 16) + return null; + + long msb = 0L; + long lsb = 0L; + for (int i = 0; i < 8; ++i) + msb += (bytes[start + i] & 0xFFL) << (56 - i * 8); + for (int i = 0; i < 8; ++i) + lsb += (bytes[start + i + 8] & 0xFFL) << (56 - i * 8); + + return new UUID(msb, lsb); + } + + public static String deviceTypeTyString(final int type) { + switch (type) { + case BluetoothDevice.DEVICE_TYPE_CLASSIC: + return "CLASSIC"; + case BluetoothDevice.DEVICE_TYPE_DUAL: + return "CLASSIC and BLE"; + case BluetoothDevice.DEVICE_TYPE_LE: + return "BLE only"; + default: + return "UNKNOWN"; + } + } + + public static String bondingStateToString(final int state) { + switch (state) { + case BluetoothDevice.BOND_BONDING: + return "BONDING..."; + case BluetoothDevice.BOND_BONDED: + return "BONDED"; + default: + return "NOT BONDED"; + } + } + + public static String getProperties(final BluetoothGattCharacteristic characteristic) { + final int properties = characteristic.getProperties(); + final StringBuilder builder = new StringBuilder(); + if ((properties & BluetoothGattCharacteristic.PROPERTY_BROADCAST) > 0) + builder.append("B "); + if ((properties & BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS) > 0) + builder.append("E "); + if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) + builder.append("I "); + if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) + builder.append("N "); + if ((properties & BluetoothGattCharacteristic.PROPERTY_READ) > 0) + builder.append("R "); + if ((properties & BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE) > 0) + builder.append("SW "); + if ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) + builder.append("W "); + if ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) + builder.append("WNR "); + + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + builder.insert(0, "["); + builder.append("]"); + } + return builder.toString(); + } + + public static int setValue(final byte[] dest, int offset, int value, int formatType) { + int len = offset + getTypeLen(formatType); + if (len > dest.length) + return offset; + + switch (formatType) { + case BluetoothGattCharacteristic.FORMAT_SINT8: + value = intToSignedBits(value, 8); + // Fall-through intended + case BluetoothGattCharacteristic.FORMAT_UINT8: + dest[offset] = (byte) (value & 0xFF); + break; + + case BluetoothGattCharacteristic.FORMAT_SINT16: + value = intToSignedBits(value, 16); + // Fall-through intended + case BluetoothGattCharacteristic.FORMAT_UINT16: + dest[offset++] = (byte) (value & 0xFF); + dest[offset] = (byte) ((value >> 8) & 0xFF); + break; + + case FORMAT_SINT24: + value = intToSignedBits(value, 24); + // Fall-through intended + case FORMAT_UINT24: + dest[offset++] = (byte) (value & 0xFF); + dest[offset++] = (byte) ((value >> 8) & 0xFF); + dest[offset] = (byte) ((value >> 16) & 0xFF); + break; + + case FORMAT_UINT16_BIG_INDIAN: + dest[offset++] = (byte) ((value >> 8) & 0xFF); + dest[offset] = (byte) (value & 0xFF); + break; + + case BluetoothGattCharacteristic.FORMAT_SINT32: + value = intToSignedBits(value, 32); + // Fall-through intended + case BluetoothGattCharacteristic.FORMAT_UINT32: + dest[offset++] = (byte) (value & 0xFF); + dest[offset++] = (byte) ((value >> 8) & 0xFF); + dest[offset++] = (byte) ((value >> 16) & 0xFF); + dest[offset] = (byte) ((value >> 24) & 0xFF); + break; + + case FORMAT_UINT32_BIG_INDIAN: + dest[offset++] = (byte) ((value >> 24) & 0xFF); + dest[offset++] = (byte) ((value >> 16) & 0xFF); + dest[offset++] = (byte) ((value >> 8) & 0xFF); + dest[offset] = (byte) (value & 0xFF); + break; + + default: + return offset; + } + return len; + } + + public static int setValue(final byte[] dest, int offset, int mantissa, int exponent, int formatType) { + int len = offset + getTypeLen(formatType); + if (len > dest.length) + return offset; + + switch (formatType) { + case BluetoothGattCharacteristic.FORMAT_SFLOAT: + mantissa = intToSignedBits(mantissa, 12); + exponent = intToSignedBits(exponent, 4); + dest[offset++] = (byte) (mantissa & 0xFF); + dest[offset] = (byte) ((mantissa >> 8) & 0x0F); + dest[offset] += (byte) ((exponent & 0x0F) << 4); + break; + + case BluetoothGattCharacteristic.FORMAT_FLOAT: + mantissa = intToSignedBits(mantissa, 24); + exponent = intToSignedBits(exponent, 8); + dest[offset++] = (byte) (mantissa & 0xFF); + dest[offset++] = (byte) ((mantissa >> 8) & 0xFF); + dest[offset++] = (byte) ((mantissa >> 16) & 0xFF); + dest[offset] += (byte) (exponent & 0xFF); + break; + + default: + return offset; + } + + return len; + } + + public static int setValue(final byte[] dest, final int offset, final String value) { + if (value == null) + return offset; + + final byte[] valueBytes = value.getBytes(); + System.arraycopy(valueBytes, 0, dest, offset, valueBytes.length); + return offset + valueBytes.length; + } + + public static int setByteArrayValue(final byte[] dest, final int offset, final String value) { + if (value == null) + return offset; + + for (int i = 0; i < value.length(); i += 2) { + dest[offset + i / 2] = (byte) ((Character.digit(value.charAt(i), 16) << 4) + + Character.digit(value.charAt(i + 1), 16)); + } + return offset + value.length() / 2; + } + + public static int getIntValue(final byte[] source, final int offset, final int formatType) { + if ((offset + getTypeLen(formatType)) > source.length) + throw new ArrayIndexOutOfBoundsException(); + + switch (formatType) { + case BluetoothGattCharacteristic.FORMAT_UINT8: + return unsignedByteToInt(source[offset]); + + case BluetoothGattCharacteristic.FORMAT_UINT16: + return unsignedBytesToInt(source[offset], source[offset + 1]); + + case FORMAT_UINT24: + return unsignedBytesToInt(source[offset], source[offset + 1], source[offset + 2]); + + case BluetoothGattCharacteristic.FORMAT_UINT32: + return unsignedBytesToInt(source[offset], source[offset + 1], source[offset + 2], source[offset + 3]); + + case FORMAT_UINT16_BIG_INDIAN: + return unsignedBytesToInt(source[offset + 1], source[offset]); + + case FORMAT_UINT32_BIG_INDIAN: + return unsignedBytesToInt(source[offset + 3], source[offset + 2], source[offset + 1], source[offset]); + + case BluetoothGattCharacteristic.FORMAT_SINT8: + return unsignedToSigned(unsignedByteToInt(source[offset]), 8); + + case BluetoothGattCharacteristic.FORMAT_SINT16: + return unsignedToSigned(unsignedBytesToInt(source[offset], source[offset + 1]), 16); + + case FORMAT_SINT24: + return unsignedToSigned(unsignedBytesToInt(source[offset], source[offset + 1], source[offset + 2]), 24); + + case BluetoothGattCharacteristic.FORMAT_SINT32: + return unsignedToSigned(unsignedBytesToInt(source[offset], source[offset + 1], source[offset + 2], source[offset + 3]), 32); + } + return 0; + } + + public static int getMantissa(final byte[] source, final int offset, final int formatType) { + if ((offset + getTypeLen(formatType)) > source.length) + throw new ArrayIndexOutOfBoundsException(); + + switch (formatType) { + case BluetoothGattCharacteristic.FORMAT_SFLOAT: + return unsignedToSigned(unsignedByteToInt(source[offset]) + ((unsignedByteToInt(source[offset + 1]) & 0x0F) << 8), 12); + case BluetoothGattCharacteristic.FORMAT_FLOAT: + return unsignedToSigned(unsignedByteToInt(source[offset]) + (unsignedByteToInt(source[offset + 1]) << 8) + (unsignedByteToInt(source[offset + 2]) << 16), 24); + } + return 0; + } + + public static int getExponent(final byte[] source, final int offset, final int formatType) { + if ((offset + getTypeLen(formatType)) > source.length) + throw new ArrayIndexOutOfBoundsException(); + + switch (formatType) { + case BluetoothGattCharacteristic.FORMAT_SFLOAT: + return unsignedToSigned(unsignedByteToInt(source[offset + 1]) >> 4, 4); + case BluetoothGattCharacteristic.FORMAT_FLOAT: + return source[offset + 3]; + } + return 0; + } + + /** + * Returns the size of a give value type. + */ + public static int getTypeLen(int formatType) { + return formatType & 0xF; + } + + /** + * Convert a signed byte to an unsigned int. + */ + private static int unsignedByteToInt(byte b) { + return b & 0xFF; + } + + /** + * Convert signed bytes to a 16-bit unsigned int. + */ + private static int unsignedBytesToInt(byte b0, byte b1) { + return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8)); + } + + /** + * Convert signed bytes to a 24-bit unsigned int. + */ + private static int unsignedBytesToInt(byte b0, byte b1, byte b2) { + return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8)) + (unsignedByteToInt(b2) << 16); + } + + /** + * Convert signed bytes to a 32-bit unsigned int. + */ + private static int unsignedBytesToInt(byte b0, byte b1, byte b2, byte b3) { + return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8)) + (unsignedByteToInt(b2) << 16) + (unsignedByteToInt(b3) << 24); + } + + /** + * Convert an unsigned integer value to a two's-complement encoded + * signed value. + */ + private static int unsignedToSigned(int unsigned, int size) { + if ((unsigned & (1 << size - 1)) != 0) { + unsigned = -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1))); + } + return unsigned; + } + + /** + * Convert an integer into the signed bits of a given length. + */ + private static int intToSignedBits(int i, int size) { + if (i < 0) { + i = (1 << size - 1) + (i & ((1 << size - 1) - 1)); + } + return i; + } + + public static int decodeUuid16(final byte[] data, final int start) { + final int b1 = data[start] & 0xff; + final int b2 = data[start + 1] & 0xff; + + return (b2 << 8 | b1) & 0xFFFF; + } + + public static int decodeUuid32(final byte[] data, final int start) { + final int b1 = data[start] & 0xff; + final int b2 = data[start + 1] & 0xff; + final int b3 = data[start + 2] & 0xff; + final int b4 = data[start + 3] & 0xff; + + return b4 << 24 | b3 << 16 | b2 << 8 | b1; + } + + public static int intOrThrow(final Integer i) { + if (i == null) + throw new NullPointerException(); + return i; + } + + public static byte[] base64Decode(String s) { + return Base64.decode(s, Base64.DEFAULT); + } + + public static String base64Encode(byte[] b) { + return Base64.encodeToString(b, Base64.DEFAULT).trim(); + } + + public static String base64Encode(String s) { + return base64Encode(setByteArrayValue(s)); + } + + private static byte[] setByteArrayValue(String hexString) { + int len = hexString.length(); + byte[] bytes = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + + Character.digit(hexString.charAt(i + 1), 16)); + } + return bytes; + } + + public static String decodeUri(final byte[] serviceData, final int start, final int length) { + if (start < 0 || serviceData.length < start + length) + return null; + + final StringBuilder uriBuilder = new StringBuilder(); + int offset = 0; + if (offset < length) { + byte b = serviceData[start + offset++]; + String scheme = URI_SCHEMES.get(b); + if (scheme != null) { + uriBuilder.append(scheme); + if (URLUtil.isNetworkUrl(scheme)) { + return decodeUrl(serviceData, start + offset, length - 1, uriBuilder); + } else if ("urn:uuid:".equals(scheme)) { + return decodeUrnUuid(serviceData, start + offset, uriBuilder); + } + } + Log.w(TAG, "decodeUri unknown Uri scheme code=" + b); + } + return null; + } + + private static String decodeUrl(final byte[] serviceData, final int start, final int length, final StringBuilder urlBuilder) { + int offset = 0; + while (offset < length) { + byte b = serviceData[start + offset++]; + String code = URL_CODES.get(b); + if (code != null) { + urlBuilder.append(code); + } else { + urlBuilder.append((char) b); + } + } + return urlBuilder.toString(); + } + + private static String decodeUrnUuid(final byte[] serviceData, final int offset, final StringBuilder urnBuilder) { + ByteBuffer bb = ByteBuffer.wrap(serviceData); + // UUIDs are ordered as byte array, which means most significant first + bb.order(ByteOrder.BIG_ENDIAN); + long mostSignificantBytes, leastSignificantBytes; + try { + bb.position(offset); + mostSignificantBytes = bb.getLong(); + leastSignificantBytes = bb.getLong(); + } catch (BufferUnderflowException e) { + Log.w(TAG, "decodeUrnUuid BufferUnderflowException!"); + return null; + } + UUID uuid = new UUID(mostSignificantBytes, leastSignificantBytes); + urnBuilder.append(uuid.toString()); + return urnBuilder.toString(); + } + + /** + * Creates the Uri string with embedded expansion codes. + * + * @param uri to be encoded + * @return the Uri string with expansion codes. + */ + public static byte[] encodeUri(String uri) { + if (uri.length() == 0) { + return new byte[0]; + } + ByteBuffer bb = ByteBuffer.allocate(uri.length()); + // UUIDs are ordered as byte array, which means most significant first + bb.order(ByteOrder.BIG_ENDIAN); + int position = 0; + + // Add the byte code for the scheme or return null if none + Byte schemeCode = encodeUriScheme(uri); + if (schemeCode == null) { + return null; + } + String scheme = URI_SCHEMES.get(schemeCode); + bb.put(schemeCode); + position += scheme.length(); + + if (URLUtil.isNetworkUrl(scheme)) { + return encodeUrl(uri, position, bb); + } else if ("urn:uuid:".equals(scheme)) { + return encodeUrnUuid(uri, position, bb); + } + return null; + } + private static Byte encodeUriScheme(String uri) { + String lowerCaseUri = uri.toLowerCase(Locale.ENGLISH); + for (int i = 0; i < URI_SCHEMES.size(); i++) { + // get the key and value. + int key = URI_SCHEMES.keyAt(i); + String value = URI_SCHEMES.valueAt(i); + if (lowerCaseUri.startsWith(value)) { + return (byte) key; + } + } + return null; + } + + private static byte[] encodeUrl(String url, int position, ByteBuffer bb) { + while (position < url.length()) { + byte expansion = findLongestExpansion(url, position); + if (expansion >= 0) { + bb.put(expansion); + position += URL_CODES.get(expansion).length(); + } else { + bb.put((byte) url.charAt(position++)); + } + } + return byteBufferToArray(bb); + } + + private static byte[] encodeUrnUuid(String urn, int position, ByteBuffer bb) { + String uuidString = urn.substring(position, urn.length()); + UUID uuid; + try { + uuid = UUID.fromString(uuidString); + } catch (IllegalArgumentException e) { + Log.w(TAG, "encodeUrnUuid invalid urn:uuid format - " + urn); + return null; + } + // UUIDs are ordered as byte array, which means most significant first + bb.order(ByteOrder.BIG_ENDIAN); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return byteBufferToArray(bb); + } + + private static byte[] byteBufferToArray(ByteBuffer bb) { + byte[] bytes = new byte[bb.position()]; + bb.rewind(); + bb.get(bytes, 0, bytes.length); + return bytes; + } + + /** + * Finds the longest expansion from the uri at the current position. + * + * @param uriString the Uri + * @param pos start position + * @return an index in URI_MAP or 0 if none. + */ + private static byte findLongestExpansion(String uriString, int pos) { + byte expansion = -1; + int expansionLength = 0; + for (int i = 0; i < URL_CODES.size(); i++) { + // get the key and value. + int key = URL_CODES.keyAt(i); + String value = URL_CODES.valueAt(i); + if (value.length() > expansionLength && uriString.startsWith(value, pos)) { + expansion = (byte) key; + expansionLength = value.length(); + } + } + return expansion; + } + + public static byte[] toByteArray(String hexString) { + int len = hexString.length(); + byte[] bytes = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + + Character.digit(hexString.charAt(i + 1), 16)); + } + return bytes; + } + + public static int decodeUint16BigEndian(final byte[] data, final int start) { + final int b1 = data[start] & 0xff; + final int b2 = data[start + 1] & 0xff; + + return b1 << 8 | b2; + } + + /** + * This method returns the Uint32 value encoded with Big Endian. + */ + public static long decodeUint32BigEndian(final byte[] data, final int start) { + final int b1 = data[start] & 0xff; + final int b2 = data[start + 1] & 0xff; + final int b3 = data[start + 2] & 0xff; + final int b4 = data[start + 3] & 0xff; + + return (b1 << 24 | b2 << 16 | b3 << 8 | b4) & 0xFFFFFFFFL; + } + + public static float decode88FixedPointNotation(final byte[] data, final int start) { + return data[start] + (float) (data[start + 1] & 0xFF) / 256.f; + } + + public static String randomUid(int len) { + byte[] buf = new byte[len]; + new Random().nextBytes(buf); + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < len; i++) { + stringBuilder.append(String.format("%02x", buf[i])); + } + return stringBuilder.toString(); + } + + public static byte[] aes128Encrypt(byte[] data, SecretKeySpec keySpec) { + Cipher cipher; + try { + // Ignore the "ECB encryption should not be used" warning. We use exactly one block so + // the difference between ECB and CBC is just an IV or not. In addition our blocks are + // always different since they have a monotonic timestamp. Most importantly, our blocks + // aren't sensitive. Decrypting them means means knowing the beacon time and its rotation + // period. If due to ECB an attacker could find out that the beacon broadcast the same + // block a second time, all it could infer is that for some reason the clock of the beacon + // reset, which is not very helpful + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + Log.e(TAG, "Error constructing cipher instance", e); + return null; + } + + try { + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + } catch (InvalidKeyException e) { + Log.e(TAG, "Error initializing cipher instance", e); + return null; + } + + byte[] ret; + try { + ret = cipher.doFinal(data); + } catch (IllegalBlockSizeException | BadPaddingException e) { + Log.e(TAG, "Error executing cipher", e); + return null; + } + + return ret; + } + + public static byte[] aes128decrypt(byte[] data, SecretKeySpec keySpec) { + Cipher cipher; + try { + // Ignore the "ECB encryption should not be used" warning. We use exactly one block so + // the difference between ECB and CBC is just an IV or not. In addition our blocks are + // always different since they have a monotonic timestamp. Most importantly, our blocks + // aren't sensitive. Decrypting them means means knowing the beacon time and its rotation + // period. If due to ECB an attacker could find out that the beacon broadcast the same + // block a second time, all it could infer is that for some reason the clock of the beacon + // reset, which is not very helpful + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + Log.e(TAG, "Error constructing cipher instance", e); + return null; + } + + try { + cipher.init(Cipher.DECRYPT_MODE, keySpec); + } catch (InvalidKeyException e) { + Log.e(TAG, "Error initializing cipher instance", e); + return null; + } + + byte[] ret; + try { + ret = cipher.doFinal(data); + } catch (IllegalBlockSizeException | BadPaddingException e) { + Log.e(TAG, "Error executing cipher", e); + return null; + } + + return ret; + } + + public static String parse(final byte[] bytes, final int offset, final int length, final String unit) { + final String notNullUnit = unit == null ? "" : " " + unit; + + switch (length) { + case 1: + return String.valueOf(ParserUtils.getIntValue(bytes, offset, BluetoothGattCharacteristic.FORMAT_SINT8)) + notNullUnit; + case 2: + return String.valueOf(ParserUtils.getIntValue(bytes, offset, BluetoothGattCharacteristic.FORMAT_SINT16)) + notNullUnit; + case 3: + return String.valueOf(ParserUtils.getIntValue(bytes, offset, ParserUtils.FORMAT_SINT24)) + notNullUnit; + case 4: + return String.valueOf(ParserUtils.getIntValue(bytes, offset, BluetoothGattCharacteristic.FORMAT_SINT32)) + notNullUnit; + case 16: + return ParserUtils.bytesToHex(bytes, offset, length, true); + } + return "Invalid data syntax: " + ParserUtils.bytesToHex(bytes, offset, length, true); + } +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetBoldTextView.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetBoldTextView.java new file mode 100644 index 0000000..2074ad0 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetBoldTextView.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.widget; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.widget.TextView; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +public class TrebuchetBoldTextView extends TextView { + + public TrebuchetBoldTextView(Context context) { + super(context); + + init(); + } + + public TrebuchetBoldTextView(Context context, AttributeSet attrs) { + super(context, attrs); + + init(); + } + + public TrebuchetBoldTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + init(); + } + + private void init() { + if (!isInEditMode()) { + final Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), getContext().getString(R.string.font_path)); + setTypeface(typeface); + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetSwitch.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetSwitch.java new file mode 100644 index 0000000..4224c06 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetSwitch.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.widget; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.widget.Switch; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +public class TrebuchetSwitch extends Switch { + + public TrebuchetSwitch(Context context) { + super(context); + + init(); + } + + public TrebuchetSwitch(Context context, AttributeSet attrs) { + super(context, attrs); + + init(); + } + + public TrebuchetSwitch(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + init(); + } + + private void init() { + if (!isInEditMode()) { + final Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), getContext().getString(R.string.normal_font_path)); + setTypeface(typeface); + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetTextView.java b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetTextView.java new file mode 100644 index 0000000..bd7de24 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrfbeacon/nearby/widget/TrebuchetTextView.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrfbeacon.nearby.widget; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.widget.TextView; + +import no.nordicsemi.android.nrfbeacon.nearby.R; + +public class TrebuchetTextView extends TextView { + + public TrebuchetTextView(Context context) { + super(context); + + init(); + } + + public TrebuchetTextView(Context context, AttributeSet attrs) { + super(context, attrs); + + init(); + } + + public TrebuchetTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + init(); + } + + private void init() { + if (!isInEditMode()) { + final Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), getContext().getString(R.string.normal_font_path)); + setTypeface(typeface); + } + } +} diff --git a/app/src/main/res/animator/connect_animator.xml b/app/src/main/res/animator/connect_animator.xml new file mode 100644 index 0000000..29600a1 --- /dev/null +++ b/app/src/main/res/animator/connect_animator.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/tab_text_color.xml b/app/src/main/res/color/tab_text_color.xml new file mode 100644 index 0000000..57c34d7 --- /dev/null +++ b/app/src/main/res/color/tab_text_color.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/text.xml b/app/src/main/res/color/text.xml new file mode 100644 index 0000000..9de63dc --- /dev/null +++ b/app/src/main/res/color/text.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/color/text_color_primary.xml b/app/src/main/res/color/text_color_primary.xml new file mode 100644 index 0000000..d335c9e --- /dev/null +++ b/app/src/main/res/color/text_color_primary.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/text_secondary.xml b/app/src/main/res/color/text_secondary.xml new file mode 100644 index 0000000..0c5d29d --- /dev/null +++ b/app/src/main/res/color/text_secondary.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_eddystone.png b/app/src/main/res/drawable-hdpi/ic_eddystone.png new file mode 100644 index 0000000..ad85b3d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_eddystone.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_feature.png b/app/src/main/res/drawable-hdpi/ic_feature.png new file mode 100644 index 0000000..6784181 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_feature.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_help.png b/app/src/main/res/drawable-hdpi/ic_help.png new file mode 100644 index 0000000..459bed7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_help.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_about.png b/app/src/main/res/drawable-hdpi/ic_menu_about.png new file mode 100644 index 0000000..b1f1406 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_about.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_discard.png b/app/src/main/res/drawable-hdpi/ic_menu_discard.png new file mode 100644 index 0000000..703b31f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_discard.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_settings.png b/app/src/main/res/drawable-hdpi/ic_menu_settings.png new file mode 100644 index 0000000..97ded33 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_settings.png differ diff --git a/app/src/main/res/drawable-hdpi/nordic_logo.png b/app/src/main/res/drawable-hdpi/nordic_logo.png new file mode 100644 index 0000000..8b9fb39 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/nordic_logo.png differ diff --git a/app/src/main/res/drawable-hdpi/stat_sys_beacon.png b/app/src/main/res/drawable-hdpi/stat_sys_beacon.png new file mode 100644 index 0000000..a906d32 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/stat_sys_beacon.png differ diff --git a/app/src/main/res/drawable-hdpi/stat_sys_nrf_beacon.png b/app/src/main/res/drawable-hdpi/stat_sys_nrf_beacon.png new file mode 100644 index 0000000..f5d0814 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/stat_sys_nrf_beacon.png differ diff --git a/app/src/main/res/drawable-land-xhdpi/background_image.jpg b/app/src/main/res/drawable-land-xhdpi/background_image.jpg new file mode 100644 index 0000000..f8dfa38 Binary files /dev/null and b/app/src/main/res/drawable-land-xhdpi/background_image.jpg differ diff --git a/app/src/main/res/drawable-land-xhdpi/nordic_logo.png b/app/src/main/res/drawable-land-xhdpi/nordic_logo.png new file mode 100644 index 0000000..24a9d4e Binary files /dev/null and b/app/src/main/res/drawable-land-xhdpi/nordic_logo.png differ diff --git a/app/src/main/res/drawable-sw600dp-xhdpi/nordic_logo.png b/app/src/main/res/drawable-sw600dp-xhdpi/nordic_logo.png new file mode 100644 index 0000000..ec31045 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xhdpi/nordic_logo.png differ diff --git a/app/src/main/res/drawable-v21/button.xml b/app/src/main/res/drawable-v21/button.xml new file mode 100644 index 0000000..650eb91 --- /dev/null +++ b/app/src/main/res/drawable-v21/button.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/app_drive.png b/app/src/main/res/drawable-xhdpi/app_drive.png new file mode 100644 index 0000000..e640551 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/app_drive.png differ diff --git a/app/src/main/res/drawable-xhdpi/app_file_manager.png b/app/src/main/res/drawable-xhdpi/app_file_manager.png new file mode 100644 index 0000000..1f3c55b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/app_file_manager.png differ diff --git a/app/src/main/res/drawable-xhdpi/app_google_play.png b/app/src/main/res/drawable-xhdpi/app_google_play.png new file mode 100644 index 0000000..14e0258 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/app_google_play.png differ diff --git a/app/src/main/res/drawable-xhdpi/app_total_commander.png b/app/src/main/res/drawable-xhdpi/app_total_commander.png new file mode 100644 index 0000000..406c6cc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/app_total_commander.png differ diff --git a/app/src/main/res/drawable-xhdpi/background_image.jpg b/app/src/main/res/drawable-xhdpi/background_image.jpg new file mode 100644 index 0000000..2a17421 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/background_image.jpg differ diff --git a/app/src/main/res/drawable-xhdpi/beacon_sample.png b/app/src/main/res/drawable-xhdpi/beacon_sample.png new file mode 100644 index 0000000..2337061 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/beacon_sample.png differ diff --git a/app/src/main/res/drawable-xhdpi/beacon_sw1.png b/app/src/main/res/drawable-xhdpi/beacon_sw1.png new file mode 100644 index 0000000..f6e188c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/beacon_sw1.png differ diff --git a/app/src/main/res/drawable-xhdpi/beacon_sw2.png b/app/src/main/res/drawable-xhdpi/beacon_sw2.png new file mode 100644 index 0000000..ad6b2e8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/beacon_sw2.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_alarms.png b/app/src/main/res/drawable-xhdpi/ic_action_alarms.png new file mode 100644 index 0000000..8ece451 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_alarms.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_monalisa.png b/app/src/main/res/drawable-xhdpi/ic_action_monalisa.png new file mode 100644 index 0000000..8337655 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_monalisa.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_phone.png b/app/src/main/res/drawable-xhdpi/ic_action_phone.png new file mode 100644 index 0000000..33cf354 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_phone.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_small_alarms.png b/app/src/main/res/drawable-xhdpi/ic_action_small_alarms.png new file mode 100644 index 0000000..9354f4c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_small_alarms.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_small_monalisa.png b/app/src/main/res/drawable-xhdpi/ic_action_small_monalisa.png new file mode 100644 index 0000000..e9e75c3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_small_monalisa.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_small_phone.png b/app/src/main/res/drawable-xhdpi/ic_action_small_phone.png new file mode 100644 index 0000000..d63d396 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_small_phone.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_small_tasker.png b/app/src/main/res/drawable-xhdpi/ic_action_small_tasker.png new file mode 100644 index 0000000..0eed393 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_small_tasker.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_small_volume_muted.png b/app/src/main/res/drawable-xhdpi/ic_action_small_volume_muted.png new file mode 100644 index 0000000..6385e3d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_small_volume_muted.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_small_web_site.png b/app/src/main/res/drawable-xhdpi/ic_action_small_web_site.png new file mode 100644 index 0000000..573bddc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_small_web_site.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_tasker.png b/app/src/main/res/drawable-xhdpi/ic_action_tasker.png new file mode 100644 index 0000000..58b5ba4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_tasker.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_volume_muted.png b/app/src/main/res/drawable-xhdpi/ic_action_volume_muted.png new file mode 100644 index 0000000..00ab6a1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_volume_muted.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_web_site.png b/app/src/main/res/drawable-xhdpi/ic_action_web_site.png new file mode 100644 index 0000000..1ff3a60 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_web_site.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_eddystone.png b/app/src/main/res/drawable-xhdpi/ic_eddystone.png new file mode 100644 index 0000000..d4fd2ea Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_eddystone.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_event_immediate.png b/app/src/main/res/drawable-xhdpi/ic_event_immediate.png new file mode 100644 index 0000000..9057390 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_event_immediate.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_event_in_range.png b/app/src/main/res/drawable-xhdpi/ic_event_in_range.png new file mode 100644 index 0000000..7bc26a4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_event_in_range.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_event_near.png b/app/src/main/res/drawable-xhdpi/ic_event_near.png new file mode 100644 index 0000000..0da8aee Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_event_near.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_event_not_in_range.png b/app/src/main/res/drawable-xhdpi/ic_event_not_in_range.png new file mode 100644 index 0000000..1c3d0b6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_event_not_in_range.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_event_small_immediate.png b/app/src/main/res/drawable-xhdpi/ic_event_small_immediate.png new file mode 100644 index 0000000..b9cc1ff Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_event_small_immediate.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_event_small_in_range.png b/app/src/main/res/drawable-xhdpi/ic_event_small_in_range.png new file mode 100644 index 0000000..7d66926 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_event_small_in_range.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_event_small_near.png b/app/src/main/res/drawable-xhdpi/ic_event_small_near.png new file mode 100644 index 0000000..7db7695 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_event_small_near.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_event_small_not_in_range.png b/app/src/main/res/drawable-xhdpi/ic_event_small_not_in_range.png new file mode 100644 index 0000000..e2e980a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_event_small_not_in_range.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_feature.png b/app/src/main/res/drawable-xhdpi/ic_feature.png new file mode 100644 index 0000000..bfc0525 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_feature.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_help.png b/app/src/main/res/drawable-xhdpi/ic_help.png new file mode 100644 index 0000000..0e67d7c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_help.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_about.png b/app/src/main/res/drawable-xhdpi/ic_menu_about.png new file mode 100644 index 0000000..4536a8d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_about.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_discard.png b/app/src/main/res/drawable-xhdpi/ic_menu_discard.png new file mode 100644 index 0000000..9eeeed1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_discard.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_settings.png b/app/src/main/res/drawable-xhdpi/ic_menu_settings.png new file mode 100644 index 0000000..5caedc8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_settings.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rssi_0_bar.png b/app/src/main/res/drawable-xhdpi/ic_rssi_0_bar.png new file mode 100644 index 0000000..40d094f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rssi_0_bar.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rssi_1_bar.png b/app/src/main/res/drawable-xhdpi/ic_rssi_1_bar.png new file mode 100644 index 0000000..72b6996 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rssi_1_bar.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rssi_2_bars.png b/app/src/main/res/drawable-xhdpi/ic_rssi_2_bars.png new file mode 100644 index 0000000..dfa10ec Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rssi_2_bars.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rssi_3_bars.png b/app/src/main/res/drawable-xhdpi/ic_rssi_3_bars.png new file mode 100644 index 0000000..ae512db Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rssi_3_bars.png differ diff --git a/app/src/main/res/drawable-xhdpi/monalisa.png b/app/src/main/res/drawable-xhdpi/monalisa.png new file mode 100644 index 0000000..5173c87 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/monalisa.png differ diff --git a/app/src/main/res/drawable-xhdpi/nordic_logo.png b/app/src/main/res/drawable-xhdpi/nordic_logo.png new file mode 100644 index 0000000..9cdca3d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/nordic_logo.png differ diff --git a/app/src/main/res/drawable-xhdpi/stat_sys_nrf_beacon.png b/app/src/main/res/drawable-xhdpi/stat_sys_nrf_beacon.png new file mode 100644 index 0000000..cbaa268 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/stat_sys_nrf_beacon.png differ diff --git a/app/src/main/res/drawable-xhdpi/zip.png b/app/src/main/res/drawable-xhdpi/zip.png new file mode 100644 index 0000000..eb96135 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/zip.png differ diff --git a/app/src/main/res/drawable-xxhdpi/action_bar_shadow.9.png b/app/src/main/res/drawable-xxhdpi/action_bar_shadow.9.png new file mode 100644 index 0000000..c4e8083 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/action_bar_shadow.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/background_title.png b/app/src/main/res/drawable-xxhdpi/background_title.png new file mode 100644 index 0000000..db5c4f5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/background_title.png differ diff --git a/app/src/main/res/drawable-xxhdpi/beacon_anim1.png b/app/src/main/res/drawable-xxhdpi/beacon_anim1.png new file mode 100644 index 0000000..7cb9cda Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/beacon_anim1.png differ diff --git a/app/src/main/res/drawable-xxhdpi/beacon_anim2.png b/app/src/main/res/drawable-xxhdpi/beacon_anim2.png new file mode 100644 index 0000000..12e30ae Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/beacon_anim2.png differ diff --git a/app/src/main/res/drawable-xxhdpi/beacon_anim3.png b/app/src/main/res/drawable-xxhdpi/beacon_anim3.png new file mode 100644 index 0000000..c72fd9d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/beacon_anim3.png differ diff --git a/app/src/main/res/drawable-xxhdpi/beacon_anim4.png b/app/src/main/res/drawable-xxhdpi/beacon_anim4.png new file mode 100644 index 0000000..633d4d2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/beacon_anim4.png differ diff --git a/app/src/main/res/drawable-xxhdpi/bottom_shadow.9.png b/app/src/main/res/drawable-xxhdpi/bottom_shadow.9.png new file mode 100644 index 0000000..b9bcc2d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bottom_shadow.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/fab_ic_add.png b/app/src/main/res/drawable-xxhdpi/fab_ic_add.png new file mode 100644 index 0000000..73f0f12 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/fab_ic_add.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add.png b/app/src/main/res/drawable-xxhdpi/ic_add.png new file mode 100644 index 0000000..b07b0c1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete.png b/app/src/main/res/drawable-xxhdpi/ic_delete.png new file mode 100644 index 0000000..f631fd5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delete.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_eddystone.png b/app/src/main/res/drawable-xxhdpi/ic_device_eddystone.png new file mode 100644 index 0000000..45f5a8f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_eddystone.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_eddystone.png b/app/src/main/res/drawable-xxhdpi/ic_eddystone.png new file mode 100644 index 0000000..9163471 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_eddystone.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit.png b/app/src/main/res/drawable-xxhdpi/ic_edit.png new file mode 100644 index 0000000..d63e846 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_edit.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_lock.png b/app/src/main/res/drawable-xxhdpi/ic_lock.png new file mode 100644 index 0000000..9fca974 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_lock.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_settings.png b/app/src/main/res/drawable-xxhdpi/ic_menu_settings.png new file mode 100644 index 0000000..eabb0a2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_settings.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nearby_googblue.png b/app/src/main/res/drawable-xxhdpi/ic_nearby_googblue.png new file mode 100644 index 0000000..015ead2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_nearby_googblue.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_nearby_grey.png b/app/src/main/res/drawable-xxhdpi/ic_nearby_grey.png new file mode 100644 index 0000000..6f28798 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_nearby_grey.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_refresh.png b/app/src/main/res/drawable-xxhdpi/ic_refresh.png new file mode 100644 index 0000000..5032eb7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_refresh.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_show.png b/app/src/main/res/drawable-xxhdpi/ic_show.png new file mode 100644 index 0000000..42b9679 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_show.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_unlock.png b/app/src/main/res/drawable-xxhdpi/ic_unlock.png new file mode 100644 index 0000000..f4996f7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_unlock.png differ diff --git a/app/src/main/res/drawable-xxhdpi/no_beacons.png b/app/src/main/res/drawable-xxhdpi/no_beacons.png new file mode 100644 index 0000000..3fae63e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/no_beacons.png differ diff --git a/app/src/main/res/drawable/app_file_browser.xml b/app/src/main/res/drawable/app_file_browser.xml new file mode 100644 index 0000000..53295be --- /dev/null +++ b/app/src/main/res/drawable/app_file_browser.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/background.xml b/app/src/main/res/drawable/background.xml new file mode 100644 index 0000000..a98cbfc --- /dev/null +++ b/app/src/main/res/drawable/background.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button.xml b/app/src/main/res/drawable/button.xml new file mode 100644 index 0000000..5428956 --- /dev/null +++ b/app/src/main/res/drawable/button.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/button_n.xml b/app/src/main/res/drawable/button_n.xml new file mode 100644 index 0000000..c48c623 --- /dev/null +++ b/app/src/main/res/drawable/button_n.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/button_p.xml b/app/src/main/res/drawable/button_p.xml new file mode 100644 index 0000000..e30a30f --- /dev/null +++ b/app/src/main/res/drawable/button_p.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/edit_text_bg.xml b/app/src/main/res/drawable/edit_text_bg.xml new file mode 100644 index 0000000..857c95d --- /dev/null +++ b/app/src/main/res/drawable/edit_text_bg.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/drawable/header_background.xml b/app/src/main/res/drawable/header_background.xml new file mode 100644 index 0000000..3fd4e5f --- /dev/null +++ b/app/src/main/res/drawable/header_background.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action.xml b/app/src/main/res/drawable/ic_action.xml new file mode 100644 index 0000000..f2d17b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_action.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_action_small.xml b/app/src/main/res/drawable/ic_action_small.xml new file mode 100644 index 0000000..559c506 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_small.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_device_bg.xml b/app/src/main/res/drawable/ic_device_bg.xml new file mode 100644 index 0000000..f5c9d1d --- /dev/null +++ b/app/src/main/res/drawable/ic_device_bg.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_event.xml b/app/src/main/res/drawable/ic_event.xml new file mode 100644 index 0000000..b4d8ff4 --- /dev/null +++ b/app/src/main/res/drawable/ic_event.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_event_small.xml b/app/src/main/res/drawable/ic_event_small.xml new file mode 100644 index 0000000..242e2a8 --- /dev/null +++ b/app/src/main/res/drawable/ic_event_small.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_rssi_bar.xml b/app/src/main/res/drawable/ic_rssi_bar.xml new file mode 100644 index 0000000..1580d5e --- /dev/null +++ b/app/src/main/res/drawable/ic_rssi_bar.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 0000000..bbae0b2 --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_update.xml b/app/src/main/res/layout-land/fragment_update.xml new file mode 100644 index 0000000..b056d00 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_update.xml @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +