diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/DeviceAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/DeviceAdapter.java new file mode 100644 index 00000000..3062789e --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/DeviceAdapter.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2016, 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.nrftoolbox.proximity; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.List; + +import no.nordicsemi.android.nrftoolbox.R; + +public class DeviceAdapter extends RecyclerView.Adapter { + private final ProximityService.ProximityBinder mService; + private final List mDevices; + + public DeviceAdapter(final ProximityService.ProximityBinder binder) { + mService = binder; + mDevices = mService.getManagedDevices(); + } + + @Override + public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { + final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_feature_proximity_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, final int position) { + holder.bind(mDevices.get(position)); + } + + @Override + public int getItemCount() { + return mDevices.size(); + } + + public void onDeviceAdded(final BluetoothDevice device) { + notifyItemInserted(mDevices.size() - 1); + } + + public void onDeviceRemoved(final BluetoothDevice device) { + notifyDataSetChanged(); // we don't have position of the removed device here + } + + public void onDeviceStateChanged(final BluetoothDevice device) { + final int position = mDevices.indexOf(device); + if (position >= 0) + notifyItemChanged(position); + } + + public void onBatteryValueReceived(final BluetoothDevice device) { + final int position = mDevices.indexOf(device); + if (position >= 0) + notifyItemChanged(position); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + private ImageView iconView; + private TextView nameView; + private TextView addressView; + private TextView batteryView; + private ImageButton actionButton; + + public ViewHolder(final View itemView) { + super(itemView); + + iconView = (ImageView) itemView.findViewById(R.id.icon); + nameView = (TextView) itemView.findViewById(R.id.name); + addressView = (TextView) itemView.findViewById(R.id.address); + batteryView = (TextView) itemView.findViewById(R.id.battery); + actionButton = (ImageButton) itemView.findViewById(R.id.action_find_silent); + + // Configure FIND / SILENT button + actionButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + final int position = getAdapterPosition(); + final BluetoothDevice device = mDevices.get(position); + final boolean on = mService.toggleImmediateAlert(device); + + actionButton.setImageResource(on ? R.drawable.ic_stat_notify_proximity_silent : R.drawable.ic_stat_notify_proximity_find); + } + }); + + // Configure Disconnect button + itemView.findViewById(R.id.action_disconnect).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + final int position = getAdapterPosition(); + final BluetoothDevice device = mDevices.get(position); + mService.disconnect(device); + } + }); + } + + private void bind(final BluetoothDevice device) { + final int state = mService.getConnectionState(device); + switch (state) { + case BluetoothGatt.STATE_DISCONNECTED: + case BluetoothGatt.STATE_DISCONNECTING: + DrawableCompat.setTint(iconView.getDrawable(), ContextCompat.getColor(iconView.getContext(), android.R.color.black)); + break; + case BluetoothGatt.STATE_CONNECTING: + DrawableCompat.setTint(iconView.getDrawable(), ContextCompat.getColor(iconView.getContext(), R.color.actionBarColor)); + break; + case BluetoothGatt.STATE_CONNECTED: + DrawableCompat.setTint(iconView.getDrawable(), ContextCompat.getColor(iconView.getContext(), R.color.actionBarColorDark)); + break; + } + + String name = device.getName(); + if (TextUtils.isEmpty(name)) + name = nameView.getResources().getString(R.string.proximity_default_device_name); + nameView.setText(name); + addressView.setText(device.getAddress()); + + final boolean on = mService.isImmediateAlertOn(device); + actionButton.setImageResource(on ? R.drawable.ic_stat_notify_proximity_silent : R.drawable.ic_stat_notify_proximity_find); + actionButton.setVisibility(state == BluetoothGatt.STATE_CONNECTED ? View.VISIBLE : View.GONE); + + final int batteryValue = mService.getBatteryValue(device); + if (batteryValue >= 0) { + batteryView.getCompoundDrawables()[0 /*left*/].setLevel(batteryValue); + batteryView.setVisibility(View.VISIBLE); + batteryView.setText(batteryView.getResources().getString(R.string.battery, batteryValue)); + batteryView.setAlpha(state == BluetoothGatt.STATE_CONNECTED ? 1.0f : 0.5f); + } + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityActivity.java index c1dd5ab8..7b057ded 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityActivity.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityActivity.java @@ -22,30 +22,26 @@ package no.nordicsemi.android.nrftoolbox.proximity; import android.bluetooth.BluetoothDevice; -import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.ImageView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import java.util.UUID; import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; -import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; +import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileService; +import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileServiceReadyActivity; +import no.nordicsemi.android.nrftoolbox.widget.DividerItemDecoration; -public class ProximityActivity extends BleProfileServiceReadyActivity { +public class ProximityActivity extends BleMulticonnectProfileServiceReadyActivity { private static final String TAG = "ProximityActivity"; - public static final String PREFS_GATT_SERVER_ENABLED = "prefs_gatt_server_enabled"; + // This is not used any more. Server is created always after the service is started or + // after Bluetooth adapter is enabled. + // public static final String PREFS_GATT_SERVER_ENABLED = "prefs_gatt_server_enabled"; - private Button mFindMeButton; - private ImageView mLockImage; - private CheckBox mGattServerSwitch; + private RecyclerView mDevicesView; + private DeviceAdapter mAdapter; @Override protected void onCreateView(final Bundle savedInstanceState) { @@ -54,18 +50,9 @@ public class ProximityActivity extends BleProfileServiceReadyActivity getServiceClass() { + protected Class getServiceClass() { return ProximityService.class; } @@ -101,102 +80,59 @@ public class ProximityActivity extends BleProfileServiceReadyActivity { private final String TAG = "ProximityManager"; @@ -57,16 +46,14 @@ public class ProximityManager extends BleManager { private static final UUID ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb"); private final static byte[] HIGH_ALERT = { 0x02 }; + private final static byte[] MILD_ALERT = { 0x01 }; private final static byte[] NO_ALERT = { 0x00 }; private BluetoothGattCharacteristic mAlertLevelCharacteristic, mLinklossCharacteristic; - private BluetoothGattServer mBluetoothGattServer; - private BluetoothDevice mDeviceToConnect; - private Handler mHandler; + private boolean mAlertOn; - public ProximityManager(Context context) { + public ProximityManager(final Context context) { super(context); - mHandler = new Handler(); } @Override @@ -74,198 +61,6 @@ public class ProximityManager extends BleManager { return true; } - private void openGattServer(Context context, BluetoothManager manager) { - mBluetoothGattServer = manager.openGattServer(context, mGattServerCallbacks); - } - - private void closeGattServer() { - if (mBluetoothGattServer != null) { - // mBluetoothGattServer.cancelConnection(mBluetoothGatt.getDevice()); // FIXME this method does not cancel the connection - mBluetoothGattServer.close(); // FIXME This method does not cause BluetoothGattServerCallback#onConnectionStateChange(newState=DISCONNECTED) to be called on Nexus phones. - mBluetoothGattServer = null; - } - } - - private void addImmediateAlertService() { - /* - * This method must be called in UI thread. It works fine on Nexus devices but if called from other thread (f.e. from onServiceAdded in gatt server callback) it hangs the app. - */ - final BluetoothGattCharacteristic alertLevel = new BluetoothGattCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, - BluetoothGattCharacteristic.PERMISSION_WRITE); - alertLevel.setValue(HIGH_ALERT); - final BluetoothGattService immediateAlertService = new BluetoothGattService(IMMEDIATE_ALERT_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); - immediateAlertService.addCharacteristic(alertLevel); - mBluetoothGattServer.addService(immediateAlertService); - } - - private void addLinklossService() { - /* - * This method must be called in UI thread. It works fine on Nexus devices but if called from other thread (f.e. from onServiceAdded in gatt server callback) it hangs the app. - */ - final BluetoothGattCharacteristic linklossAlertLevel = new BluetoothGattCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE - | BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ); - linklossAlertLevel.setValue(HIGH_ALERT); - final BluetoothGattService linklossService = new BluetoothGattService(LINKLOSS_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); - linklossService.addCharacteristic(linklossAlertLevel); - mBluetoothGattServer.addService(linklossService); - } - - private final BluetoothGattServerCallback mGattServerCallbacks = new BluetoothGattServerCallback() { - - @Override - public void onServiceAdded(final int status, final BluetoothGattService service) { - Logger.v(mLogSession, "[Server] Service " + service.getUuid() + " added"); - - mHandler.post(new Runnable() { - @Override - public void run() { - // Adding another service from callback thread fails on Samsung S4 with Android 4.3 - if (IMMEDIATE_ALERT_SERVICE_UUID.equals(service.getUuid())) - addLinklossService(); - else { - Logger.i(mLogSession, "[Server] Gatt server started"); - ProximityManager.super.connect(mDeviceToConnect); - mDeviceToConnect = null; - } - } - }); - } - - @Override - public void onConnectionStateChange(final BluetoothDevice device, final int status, final int newState) { - Logger.d(mLogSession, "[Server callback] Connection state changed with status: " + status + " and new state: " + stateToString(newState) + " (" + newState + ")"); - if (status == BluetoothGatt.GATT_SUCCESS) { - if (newState == BluetoothGatt.STATE_CONNECTED) { - Logger.i(mLogSession, "[Server] Device with address " + device.getAddress() + " connected"); - } else { - Logger.i(mLogSession, "[Server] Device disconnected"); - } - } else { - Logger.e(mLogSession, "[Server] Error " + status + " (0x" + Integer.toHexString(status) + "): " + GattError.parseConnectionError(status)); - } - } - - @Override - public void onCharacteristicReadRequest(final BluetoothDevice device, final int requestId, final int offset, final BluetoothGattCharacteristic characteristic) { - Logger.d(mLogSession, "[Server callback] Read request for characteristic " + characteristic.getUuid() + " (requestId=" + requestId + ", offset=" + offset + ")"); - Logger.i(mLogSession, "[Server] READ request for characteristic " + characteristic.getUuid() + " received"); - - byte[] value = characteristic.getValue(); - if (value != null && offset > 0) { - byte[] offsetValue = new byte[value.length - offset]; - System.arraycopy(value, offset, offsetValue, 0, offsetValue.length); - value = offsetValue; - } - if (value != null) - Logger.d(mLogSession, "server.sendResponse(GATT_SUCCESS, value=" + ParserUtils.parse(value) + ")"); - else - Logger.d(mLogSession, "server.sendResponse(GATT_SUCCESS, value=null)"); - mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); - Logger.v(mLogSession, "[Server] Response sent"); - } - - @Override - public void onCharacteristicWriteRequest(final BluetoothDevice device, final int requestId, final BluetoothGattCharacteristic characteristic, final boolean preparedWrite, - final boolean responseNeeded, final int offset, final byte[] value) { - Logger.d(mLogSession, "[Server callback] Write request to characteristic " + characteristic.getUuid() - + " (requestId=" + requestId + ", prepareWrite=" + preparedWrite + ", responseNeeded=" + responseNeeded + ", offset=" + offset + ", value=" + ParserUtils.parse(value) + ")"); - final String writeType = !responseNeeded ? "WRITE NO RESPONSE" : "WRITE COMMAND"; - Logger.i(mLogSession, "[Server] " + writeType + " request for characteristic " + characteristic.getUuid() + " received, value: " + ParserUtils.parse(value)); - - if (offset == 0) { - characteristic.setValue(value); - } else { - final byte[] currentValue = characteristic.getValue(); - final byte[] newValue = new byte[currentValue.length + value.length]; - System.arraycopy(currentValue, 0, newValue, 0, currentValue.length); - System.arraycopy(value, 0, newValue, offset, value.length); - characteristic.setValue(newValue); - } - - if (!preparedWrite && value != null && value.length == 1) { // small validation - if (value[0] != NO_ALERT[0]) { - Logger.a(mLogSession, "[Server] Immediate alarm request received: " + AlertLevelParser.parse(characteristic)); - mCallbacks.onAlarmTriggered(device); - } else { - Logger.a(mLogSession, "[Server] Immediate alarm request received: OFF"); - mCallbacks.onAlarmStopped(device); - } - } - - Logger.d(mLogSession, "server.sendResponse(GATT_SUCCESS, offset=" + offset + ", value=" + ParserUtils.parse(value) + ")"); - mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null); - Logger.v(mLogSession, "[Server] Response sent"); - } - - @Override - public void onDescriptorReadRequest(final BluetoothDevice device, final int requestId, final int offset, final BluetoothGattDescriptor descriptor) { - Logger.d(mLogSession, "[Server callback] Write request to descriptor " + descriptor.getUuid() + " (requestId=" + requestId + ", offset=" + offset + ")"); - Logger.i(mLogSession, "[Server] READ request for descriptor " + descriptor.getUuid() + " received"); - // This method is not supported - Logger.w(mLogSession, "[Server] Operation not supported"); - Logger.d(mLogSession, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)"); - mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, null); - Logger.v(mLogSession, "[Server] Response sent"); - } - - @Override - public void onDescriptorWriteRequest(final BluetoothDevice device, final int requestId, final BluetoothGattDescriptor descriptor, final boolean preparedWrite, - final boolean responseNeeded, final int offset, final byte[] value) { - Logger.d(mLogSession, "[Server callback] Write request to descriptor " + descriptor.getUuid() - + " (requestId=" + requestId + ", prepareWrite=" + preparedWrite + ", responseNeeded=" + responseNeeded + ", offset=" + offset + ", value=" + ParserUtils.parse(value) + ")"); - Logger.i(mLogSession, "[Server] READ request for descriptor " + descriptor.getUuid() + " received"); - // This method is not supported - Logger.w(mLogSession, "[Server] Operation not supported"); - Logger.d(mLogSession, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)"); - mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, null); - Logger.v(mLogSession, "[Server] Response sent"); - } - - @Override - public void onExecuteWrite(final BluetoothDevice device, final int requestId, final boolean execute) { - Logger.d(mLogSession, "[Server callback] Execute write request (requestId=" + requestId + ", execute=" + execute + ")"); - // This method is not supported - Logger.w(mLogSession, "[Server] Operation not supported"); - Logger.d(mLogSession, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)"); - mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, 0, null); - Logger.v(mLogSession, "[Server] Response sent"); - } - }; - - @Override - public void connect(final BluetoothDevice device) { - // Should we use the GATT Server? - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - final boolean useGattServer = preferences.getBoolean(ProximityActivity.PREFS_GATT_SERVER_ENABLED, true); - - if (useGattServer) { - // Save the device that we want to connect to. First we will create a GATT Server - mDeviceToConnect = device; - - final BluetoothManager bluetoothManager = (BluetoothManager) getContext().getSystemService(Context.BLUETOOTH_SERVICE); - try { - DebugLogger.d(TAG, "[Server] Starting Gatt server..."); - Logger.v(mLogSession, "[Server] Starting Gatt server..."); - openGattServer(getContext(), bluetoothManager); - addImmediateAlertService(); - // the BluetoothGattServerCallback#onServiceAdded callback will proceed further operations - } catch (final Exception e) { - // On Nexus 4&7 with Android 4.4 (build KRT16S) sometimes creating Gatt Server fails. There is a Null Pointer Exception thrown from addCharacteristic method. - Logger.e(mLogSession, "[Server] Gatt server failed to start"); - Log.e(TAG, "Creating Gatt Server failed", e); - } - } else { - super.connect(device); - } - } - - @Override - public boolean disconnect() { - final boolean result = super.disconnect(); - closeGattServer(); - return result; - } - @Override protected BleManagerGattCallback getGattCallback() { return mGattCallback; @@ -313,31 +108,36 @@ public class ProximityManager extends BleManager { } }; - public void writeImmediateAlertOn() { + /** + * Toggles the immediate alert on the target device. + * @return true if alarm has been enabled, false if disabled + */ + public boolean toggleImmediateAlert() { + writeImmediateAlert(!mAlertOn); + return mAlertOn; + } + + /** + * Writes the HIGH ALERT or NO ALERT command to the target device + * @param on true to enable the alarm on proximity tag, false to disable it + */ + public void writeImmediateAlert(final boolean on) { + if (!isConnected()) + return; + if (mAlertLevelCharacteristic != null) { - mAlertLevelCharacteristic.setValue(HIGH_ALERT); + mAlertLevelCharacteristic.setValue(on ? HIGH_ALERT : NO_ALERT); writeCharacteristic(mAlertLevelCharacteristic); + mAlertOn = on; } else { DebugLogger.w(TAG, "Immediate Alert Level Characteristic is not found"); } } - public void writeImmediateAlertOff() { - if (mAlertLevelCharacteristic != null) { - mAlertLevelCharacteristic.setValue(NO_ALERT); - writeCharacteristic(mAlertLevelCharacteristic); - } else { - DebugLogger.w(TAG, "Immediate Alert Level Characteristic is not found"); - } - } - - @Override - public void close() { - super.close(); - - if (mBluetoothGattServer != null) { - mBluetoothGattServer.close(); - mBluetoothGattServer = null; - } + /** + * Returns true if the alert has been enabled on the proximity tag, false otherwise. + */ + public boolean isAlertEnabled() { + return mAlertOn; } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManagerCallbacks.java index 7fe33dc6..4e310bd4 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManagerCallbacks.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManagerCallbacks.java @@ -21,12 +21,8 @@ */ package no.nordicsemi.android.nrftoolbox.proximity; -import android.bluetooth.BluetoothDevice; - import no.nordicsemi.android.nrftoolbox.profile.BleManagerCallbacks; public interface ProximityManagerCallbacks extends BleManagerCallbacks { - void onAlarmTriggered(final BluetoothDevice device); - - void onAlarmStopped(final BluetoothDevice device); + // No additional methods } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManager.java new file mode 100644 index 00000000..e2cdbad6 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManager.java @@ -0,0 +1,262 @@ +/* + * 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.nrftoolbox.proximity; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattServer; +import android.bluetooth.BluetoothGattServerCallback; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.os.Handler; + +import java.util.UUID; + +import no.nordicsemi.android.error.GattError; +import no.nordicsemi.android.log.LogContract; +import no.nordicsemi.android.nrftoolbox.parser.AlertLevelParser; +import no.nordicsemi.android.nrftoolbox.profile.multiconnect.IDeviceLogger; +import no.nordicsemi.android.nrftoolbox.utility.ParserUtils; + +public class ProximityServerManager { + private final String TAG = "ProximityServerManager"; + + /** Immediate Alert service UUID */ + public final static UUID IMMEDIATE_ALERT_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb"); + /** Linkloss service UUID */ + public final static UUID LINKLOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb"); + /** Alert Level characteristic UUID */ + private static final UUID ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb"); + + private final static byte[] HIGH_ALERT = { 0x02 }; + private final static byte[] MILD_ALERT = { 0x01 }; + private final static byte[] NO_ALERT = { 0x00 }; + + private BluetoothGattServer mBluetoothGattServer; + private ProximityServerManagerCallbacks mCallbacks; + private IDeviceLogger mLogger; + private Handler mHandler; + private OnServerOpenCallback mOnServerOpenCallback; + + public interface OnServerOpenCallback { + /** Method called when the GATT server was created and all services were added successfully. */ + void onGattServerOpen(); + } + + public ProximityServerManager(final ProximityServerManagerCallbacks callbacks) { + mHandler = new Handler(); + mCallbacks = callbacks; + } + + public void setLogger(final IDeviceLogger logger) { + mLogger = logger; + } + + public void openGattServer(final Context context, final OnServerOpenCallback callback) { + mOnServerOpenCallback = callback; + + final BluetoothManager manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + mBluetoothGattServer = manager.openGattServer(context, mGattServerCallbacks); + addImmediateAlertService(); + } + + public void closeGattServer() { + if (mBluetoothGattServer != null) { + mBluetoothGattServer.close(); + mBluetoothGattServer = null; + mOnServerOpenCallback = null; + } + } + + public void cancelConnection(final BluetoothDevice device) { + if (mBluetoothGattServer != null) { + mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Cancelling server connection..."); + mLogger.log(device, LogContract.Log.Level.DEBUG, "server.cancelConnection(device)"); + mBluetoothGattServer.cancelConnection(device); + } + } + + private void addImmediateAlertService() { + /* + * This method must be called in UI thread. It works fine on Nexus devices but if called from other thread (e.g. from onServiceAdded in gatt server callback) it hangs the app. + */ + final BluetoothGattCharacteristic alertLevel = new BluetoothGattCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, + BluetoothGattCharacteristic.PERMISSION_WRITE); + alertLevel.setValue(NO_ALERT); + final BluetoothGattService immediateAlertService = new BluetoothGattService(IMMEDIATE_ALERT_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); + immediateAlertService.addCharacteristic(alertLevel); + mBluetoothGattServer.addService(immediateAlertService); + } + + private void addLinklossService() { + /* + * This method must be called in UI thread. It works fine on Nexus devices but if called from other thread (e.g. from onServiceAdded in gatt server callback) it hangs the app. + */ + final BluetoothGattCharacteristic linklossAlertLevel = new BluetoothGattCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE + | BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ); + linklossAlertLevel.setValue(HIGH_ALERT); + final BluetoothGattService linklossService = new BluetoothGattService(LINKLOSS_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); + linklossService.addCharacteristic(linklossAlertLevel); + mBluetoothGattServer.addService(linklossService); + } + + private final BluetoothGattServerCallback mGattServerCallbacks = new BluetoothGattServerCallback() { + @Override + public void onServiceAdded(final int status, final BluetoothGattService service) { + // Adding another service from callback thread fails on Samsung S4 with Android 4.3 + mHandler.post(new Runnable() { + @Override + public void run() { + if (IMMEDIATE_ALERT_SERVICE_UUID.equals(service.getUuid())) { + addLinklossService(); + } else if (mOnServerOpenCallback != null) { + mOnServerOpenCallback.onGattServerOpen(); + mOnServerOpenCallback = null; + } + } + }); + } + + @Override + public void onConnectionStateChange(final BluetoothDevice device, final int status, final int newState) { + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server callback] Connection state changed with status: " + status + " and new state: " + stateToString(newState) + " (" + newState + ")"); + if (status == BluetoothGatt.GATT_SUCCESS) { + if (newState == BluetoothGatt.STATE_CONNECTED) { + mLogger.log(device, LogContract.Log.Level.INFO, "[Server] Device with address " + device.getAddress() + " connected"); + } else { + mLogger.log(device, LogContract.Log.Level.INFO, "[Server] Device disconnected"); + mCallbacks.onAlarmStopped(device); + } + } else { + mLogger.log(device, LogContract.Log.Level.ERROR, "[Server] Error " + status + " (0x" + Integer.toHexString(status) + "): " + GattError.parseConnectionError(status)); + } + } + + @Override + public void onCharacteristicReadRequest(final BluetoothDevice device, final int requestId, final int offset, final BluetoothGattCharacteristic characteristic) { + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server callback] Read request for characteristic " + characteristic.getUuid() + " (requestId=" + requestId + ", offset=" + offset + ")"); + mLogger.log(device, LogContract.Log.Level.INFO, "[Server] READ request for characteristic " + characteristic.getUuid() + " received"); + + byte[] value = characteristic.getValue(); + if (value != null && offset > 0) { + byte[] offsetValue = new byte[value.length - offset]; + System.arraycopy(value, offset, offsetValue, 0, offsetValue.length); + value = offsetValue; + } + if (value != null) + mLogger.log(device, LogContract.Log.Level.DEBUG, "server.sendResponse(GATT_SUCCESS, value=" + ParserUtils.parseDebug(value) + ")"); + else + mLogger.log(device, LogContract.Log.Level.DEBUG, "server.sendResponse(GATT_SUCCESS, value=null)"); + mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); + mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent"); + } + + @Override + public void onCharacteristicWriteRequest(final BluetoothDevice device, final int requestId, final BluetoothGattCharacteristic characteristic, final boolean preparedWrite, + final boolean responseNeeded, final int offset, final byte[] value) { + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server callback] Write request to characteristic " + characteristic.getUuid() + + " (requestId=" + requestId + ", prepareWrite=" + preparedWrite + ", responseNeeded=" + responseNeeded + ", offset=" + offset + ", value=" + ParserUtils.parseDebug(value) + ")"); + final String writeType = !responseNeeded ? "WRITE NO RESPONSE" : "WRITE COMMAND"; + mLogger.log(device, LogContract.Log.Level.INFO, "[Server] " + writeType + " request for characteristic " + characteristic.getUuid() + " received, value: " + ParserUtils.parse(value)); + + if (offset == 0) { + characteristic.setValue(value); + } else { + final byte[] currentValue = characteristic.getValue(); + final byte[] newValue = new byte[currentValue.length + value.length]; + System.arraycopy(currentValue, 0, newValue, 0, currentValue.length); + System.arraycopy(value, 0, newValue, offset, value.length); + characteristic.setValue(newValue); + } + + if (!preparedWrite && value != null && value.length == 1) { // small validation + if (value[0] != NO_ALERT[0]) { + mLogger.log(device, LogContract.Log.Level.APPLICATION, "[Server] Immediate alarm request received: " + AlertLevelParser.parse(characteristic)); + mCallbacks.onAlarmTriggered(device); + } else { + mLogger.log(device, LogContract.Log.Level.APPLICATION, "[Server] Immediate alarm request received: OFF"); + mCallbacks.onAlarmStopped(device); + } + } + + mLogger.log(device, LogContract.Log.Level.DEBUG, "server.sendResponse(GATT_SUCCESS, offset=" + offset + ", value=" + ParserUtils.parseDebug(value) + ")"); + mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null); + mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent"); + } + + @Override + public void onDescriptorReadRequest(final BluetoothDevice device, final int requestId, final int offset, final BluetoothGattDescriptor descriptor) { + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server callback] Write request to descriptor " + descriptor.getUuid() + " (requestId=" + requestId + ", offset=" + offset + ")"); + mLogger.log(device, LogContract.Log.Level.INFO, "[Server] READ request for descriptor " + descriptor.getUuid() + " received"); + // This method is not supported + mLogger.log(device, LogContract.Log.Level.WARNING, "[Server] Operation not supported"); + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)"); + mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, null); + mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent"); + } + + @Override + public void onDescriptorWriteRequest(final BluetoothDevice device, final int requestId, final BluetoothGattDescriptor descriptor, final boolean preparedWrite, + final boolean responseNeeded, final int offset, final byte[] value) { + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server callback] Write request to descriptor " + descriptor.getUuid() + + " (requestId=" + requestId + ", prepareWrite=" + preparedWrite + ", responseNeeded=" + responseNeeded + ", offset=" + offset + ", value=" + ParserUtils.parse(value) + ")"); + mLogger.log(device, LogContract.Log.Level.INFO, "[Server] READ request for descriptor " + descriptor.getUuid() + " received"); + // This method is not supported + mLogger.log(device, LogContract.Log.Level.WARNING, "[Server] Operation not supported"); + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)"); + mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, null); + mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent"); + } + + @Override + public void onExecuteWrite(final BluetoothDevice device, final int requestId, final boolean execute) { + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server callback] Execute write request (requestId=" + requestId + ", execute=" + execute + ")"); + // This method is not supported + mLogger.log(device, LogContract.Log.Level.WARNING, "[Server] Operation not supported"); + mLogger.log(device, LogContract.Log.Level.DEBUG, "[Server] server.sendResponse(GATT_REQUEST_NOT_SUPPORTED)"); + mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, 0, null); + mLogger.log(device, LogContract.Log.Level.VERBOSE, "[Server] Response sent"); + } + }; + + /** + * Converts the connection state to String value + * @param state the connection state + * @return state as String + */ + private String stateToString(final int state) { + switch (state) { + case BluetoothProfile.STATE_CONNECTED: + return "CONNECTED"; + case BluetoothProfile.STATE_CONNECTING: + return "CONNECTING"; + case BluetoothProfile.STATE_DISCONNECTING: + return "DISCONNECTING"; + default: + return "DISCONNECTED"; + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManagerCallbacks.java new file mode 100644 index 00000000..e629173a --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManagerCallbacks.java @@ -0,0 +1,32 @@ +/* + * 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.nrftoolbox.proximity; + +import android.bluetooth.BluetoothDevice; + +import no.nordicsemi.android.nrftoolbox.profile.BleManagerCallbacks; + +public interface ProximityServerManagerCallbacks extends BleManagerCallbacks { + void onAlarmTriggered(final BluetoothDevice device); + + void onAlarmStopped(final BluetoothDevice device); +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java index 53605e62..5f0e6268 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java @@ -34,50 +34,69 @@ import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.content.ContextCompat; import android.support.v7.app.NotificationCompat; +import android.text.TextUtils; -import no.nordicsemi.android.log.Logger; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import no.nordicsemi.android.log.LogContract; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; import no.nordicsemi.android.nrftoolbox.R; import no.nordicsemi.android.nrftoolbox.profile.BleManager; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; +import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileService; -public class ProximityService extends BleProfileService implements ProximityManagerCallbacks { +public class ProximityService extends BleMulticonnectProfileService implements ProximityManagerCallbacks, ProximityServerManagerCallbacks { @SuppressWarnings("unused") private static final String TAG = "ProximityService"; private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_DISCONNECT"; - private final static String ACTION_FIND_ME = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_FIND_ME"; - private final static String ACTION_SILENT_ME = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_SILENT_ME"; + private final static String ACTION_FIND = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_FIND"; + private final static String ACTION_SILENT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_SILENT"; + private final static String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.proximity.EXTRA_DEVICE"; - private ProximityManager mProximityManager; - private Ringtone mRingtoneNotification; - private Ringtone mRingtoneAlarm; - private boolean isImmediateAlertOn = false; - - private final static int NOTIFICATION_ID = 100; + private final static String PROXIMITY_GROUP_ID = "proximity_connected_tags"; + private final static int NOTIFICATION_ID = 1000; private final static int OPEN_ACTIVITY_REQ = 0; private final static int DISCONNECT_REQ = 1; - private final static int FIND_ME_REQ = 2; - private final static int SILENT_ME_REQ = 3; + private final static int FIND_REQ = 2; + private final static int SILENT_REQ = 3; - private final LocalBinder mBinder = new ProximityBinder(); + private final ProximityBinder mBinder = new ProximityBinder(); + private ProximityServerManager mServerManager; + private Ringtone mRingtoneAlarm; + /** + * When a device starts an alarm on the phone it is added to this list. + * Alarm is disabled when this list is empty. + */ + private List mDevicesWithAlarm; /** * This local binder is an interface for the bonded activity to operate with the proximity sensor */ public class ProximityBinder extends LocalBinder { - public boolean toggleImmediateAlert() { - if (isImmediateAlertOn) { - stopImmediateAlert(); - } else { - startImmediateAlert(); - } - return isImmediateAlertOn; // this value is changed by methods above + /** + * Toggles the Immediate Alert on given remote device. + * @param device the connected device + * @return true if alarm has been enabled, false if disabled + */ + public boolean toggleImmediateAlert(final BluetoothDevice device) { + final ProximityManager manager = (ProximityManager) getBleManager(device); + return manager.toggleImmediateAlert(); } - public boolean isImmediateAlertOn() { - return isImmediateAlertOn; + /** + * Returns the current alarm state on given device. This value is not read from the device, it's just the last value written to it + * (initially false). + * @param device the connected device + * @return true if alarm has been enabled, false if disabled + */ + public boolean isImmediateAlertOn(final BluetoothDevice device) { + final ProximityManager manager = (ProximityManager) getBleManager(device); + return manager.isAlertEnabled(); } } @@ -88,7 +107,7 @@ public class ProximityService extends BleProfileService implements ProximityMana @Override protected BleManager initializeManager() { - return mProximityManager = new ProximityManager(this); + return new ProximityManager(this); } /** @@ -97,192 +116,322 @@ public class ProximityService extends BleProfileService implements ProximityMana private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); + final BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); + mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] DISCONNECT action pressed"); + mBinder.disconnect(device); } }; /** - * This broadcast receiver listens for {@link #ACTION_FIND_ME} that may be fired by pressing Find me action button on the notification. + * This broadcast receiver listens for {@link #ACTION_FIND} or {@link #ACTION_SILENT} that may be fired by pressing Find me action button on the notification. */ - private final BroadcastReceiver mFindMeActionBroadcastReceiver = new BroadcastReceiver() { + private final BroadcastReceiver mToggleAlarmActionBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Find Me action pressed"); - startImmediateAlert(); - } - }; - - /** - * This broadcast receiver listens for {@link #ACTION_SILENT_ME} that may be fired by pressing Silent Me action button on the notification. - */ - private final BroadcastReceiver mSilentMeActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Silent Me action pressed"); - stopImmediateAlert(); + final BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); + switch (intent.getAction()) { + case ACTION_FIND: + mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] FIND action pressed"); + break; + case ACTION_SILENT: + mBinder.log(device, LogContract.Log.Level.INFO, "[Notification] SILENT action pressed"); + break; + } + mBinder.toggleImmediateAlert(device); + createNotificationForConnectedDevice(device); } }; @Override - public void onCreate() { - super.onCreate(); - + protected void onServiceCreated() { initializeAlarm(); registerReceiver(mDisconnectActionBroadcastReceiver, new IntentFilter(ACTION_DISCONNECT)); - registerReceiver(mFindMeActionBroadcastReceiver, new IntentFilter(ACTION_FIND_ME)); - registerReceiver(mSilentMeActionBroadcastReceiver, new IntentFilter(ACTION_SILENT_ME)); + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_FIND); + filter.addAction(ACTION_SILENT); + registerReceiver(mToggleAlarmActionBroadcastReceiver, filter); } @Override - public void onDestroy() { - cancelNotification(); - unregisterReceiver(mDisconnectActionBroadcastReceiver); - unregisterReceiver(mFindMeActionBroadcastReceiver); - unregisterReceiver(mSilentMeActionBroadcastReceiver); + public void onServiceStopped() { + cancelNotifications(); - super.onDestroy(); + // GATT server might have not been created if Bluetooth was disabled + if (mServerManager != null) { + mServerManager.closeGattServer(); + } + + unregisterReceiver(mDisconnectActionBroadcastReceiver); + unregisterReceiver(mToggleAlarmActionBroadcastReceiver); + + super.onServiceStopped(); + } + + @Override + protected void onBluetoothEnabled() { + super.onBluetoothEnabled(); + // Start the GATT Server only if Bluetooth is enabled + mServerManager = new ProximityServerManager(this); + mServerManager.setLogger(mBinder); + mServerManager.openGattServer(this, new ProximityServerManager.OnServerOpenCallback() { + @Override + public void onGattServerOpen() { + // We are now ready to reconnect devices + ProximityService.super.onBluetoothEnabled(); + } + }); + } + + @Override + protected void onBluetoothDisabled() { + super.onBluetoothDisabled(); + if (mServerManager != null) { + mServerManager.closeGattServer(); + } } @Override protected void onRebind() { // when the activity rebinds to the service, remove the notification - cancelNotification(); + cancelNotifications(); } @Override public void onUnbind() { - // when the activity closes we need to show the notification that user is connected to the sensor - if (isConnected()) - createNotification(R.string.proximity_notification_connected_message, 0); - else - createNotification(R.string.proximity_notification_linkloss_alert, 0); + createBackgroundNotification(); } @Override - public void onDeviceDisconnecting(final BluetoothDevice device) { - stopAlarm(); - } + public void onDeviceConnected(final BluetoothDevice device) { + super.onDeviceConnected(device); - @Override - public void onDeviceDisconnected(final BluetoothDevice device) { - super.onDeviceDisconnected(device); - isImmediateAlertOn = false; + if (!mBinded) { + createBackgroundNotification(); + } } @Override public void onLinklossOccur(final BluetoothDevice device) { + stopAlarm(device); super.onLinklossOccur(device); - isImmediateAlertOn = false; if (!mBinded) { - // when the activity closes we need to show the notification that user is connected to the sensor - playNotification(); - createNotification(R.string.proximity_notification_linkloss_alert, Notification.DEFAULT_ALL); + createBackgroundNotification(); + createLinklossNotification(device); + } + } + + @Override + public void onDeviceDisconnected(final BluetoothDevice device) { + if (mServerManager != null) { + mServerManager.cancelConnection(device); + } + stopAlarm(device); + super.onDeviceDisconnected(device); + + if (!mBinded) { + cancelNotification(device); + createBackgroundNotification(); } } @Override public void onAlarmTriggered(final BluetoothDevice device) { - playAlarm(); + playAlarm(device); } @Override public void onAlarmStopped(final BluetoothDevice device) { - stopAlarm(); + stopAlarm(device); } - /** - * Creates the notification - * - * @param messageResId - * message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults - * signals that will be used to notify the user - */ - private void createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, ProximityActivity.class); - - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - - PendingIntent secondAction; - if (isImmediateAlertOn) { - final Intent intent = new Intent(ACTION_SILENT_ME); - secondAction = PendingIntent.getBroadcast(this, SILENT_ME_REQ, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } else { - final Intent intent = new Intent(ACTION_FIND_ME); - secondAction = PendingIntent.getBroadcast(this, FIND_ME_REQ, intent, PendingIntent.FLAG_UPDATE_CURRENT); + private void createBackgroundNotification() { + final List connectedDevices = getConnectedDevices(); + for (final BluetoothDevice device : connectedDevices) { + createNotificationForConnectedDevice(device); } + createSummaryNotification(); + } - // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); - builder.setContentIntent(pendingIntent); - builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); - builder.setSmallIcon(R.drawable.ic_stat_notify_proximity); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(defaults == 0); // an ongoing notification would not be shown on Android Wear - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.proximity_notification_action_disconnect), disconnectAction)); - if (isConnected()) - builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity, getString(isImmediateAlertOn ? R.string.proximity_action_silentme : R.string.proximity_action_findme), secondAction)); + private void createSummaryNotification() { + final NotificationCompat.Builder builder = getNotificationBuilder(); + builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark)); + builder.setShowWhen(false).setDefaults(0).setOngoing(true); // an ongoing notification will not be shown on Android Wear + builder.setGroup(PROXIMITY_GROUP_ID).setGroupSummary(true); + builder.setContentTitle(getString(R.string.app_name)); + + final List managedDevices = getManagedDevices(); + final List connectedDevices = getConnectedDevices(); + if (connectedDevices.isEmpty()) { + // No connected devices + final int numberOfManagedDevices = managedDevices.size(); + if (numberOfManagedDevices == 1) { + final String name = getDeviceName(managedDevices.get(0)); + builder.setContentText(getResources().getQuantityString(R.plurals.proximity_notification_text_nothing_connected, numberOfManagedDevices, name)); + } else { + builder.setContentText(getResources().getQuantityString(R.plurals.proximity_notification_text_nothing_connected, numberOfManagedDevices, numberOfManagedDevices)); + } + } else { + // There are some proximity tags connected + final StringBuilder text = new StringBuilder(); + + final int numberOfConnectedDevices = connectedDevices.size(); + if (numberOfConnectedDevices == 1) { + final String name = getDeviceName(connectedDevices.get(0)); + text.append(getResources().getQuantityString(R.plurals.proximity_notification_summary_text, numberOfConnectedDevices, name)); + } else { + text.append(getResources().getQuantityString(R.plurals.proximity_notification_summary_text, numberOfConnectedDevices, numberOfConnectedDevices)); + } + + // If there are some disconnected devices, also print them + final int numberOfDisconnectedDevices = managedDevices.size() - numberOfConnectedDevices; + if (numberOfDisconnectedDevices == 1) { + text.append(", "); + // Find the single disconnected device to get its name + for (final BluetoothDevice device : managedDevices) { + if (!isConnected(device)) { + final String name = getDeviceName(device); + text.append(getResources().getQuantityString(R.plurals.proximity_notification_text_nothing_connected, numberOfDisconnectedDevices, name)); + break; + } + } + } else if (numberOfDisconnectedDevices > 1) { + text.append(", "); + // If there are more, just write number of them + text.append(getResources().getQuantityString(R.plurals.proximity_notification_text_nothing_connected, numberOfDisconnectedDevices, numberOfDisconnectedDevices)); + } + builder.setContentText(text); + } final Notification notification = builder.build(); final NotificationManagerCompat nm = NotificationManagerCompat.from(this); nm.notify(NOTIFICATION_ID, notification); } + /** + * Creates the notification for given connected device. + * Adds 3 action buttons: DISCONNECT, FIND and SILENT which perform given action on the device. + */ + private void createNotificationForConnectedDevice(final BluetoothDevice device) { + final NotificationCompat.Builder builder = getNotificationBuilder(); + builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark)); + builder.setGroup(PROXIMITY_GROUP_ID).setDefaults(0).setOngoing(true); // an ongoing notification will not be shown on Android Wear + builder.setContentTitle(getString(R.string.proximity_notification_text, getDeviceName(device))); + + // Add DISCONNECT action + final Intent disconnect = new Intent(ACTION_DISCONNECT); + disconnect.putExtra(EXTRA_DEVICE, device); + final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ + device.hashCode(), disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.proximity_action_disconnect), disconnectAction)); + builder.setSortKey(getDeviceName(device) + device.getAddress()); // This will keep the same order of notification even after an action was clicked on one of them + + // Add FIND or SILENT action + final ProximityManager manager = (ProximityManager) getBleManager(device); + if (manager.isAlertEnabled()) { + final Intent silentAllIntent = new Intent(ACTION_SILENT); + silentAllIntent.putExtra(EXTRA_DEVICE, device); + final PendingIntent silentAction = PendingIntent.getBroadcast(this, SILENT_REQ + device.hashCode(), silentAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_silent, getString(R.string.proximity_action_silent), silentAction)); + } else { + final Intent findAllIntent = new Intent(ACTION_FIND); + findAllIntent.putExtra(EXTRA_DEVICE, device); + final PendingIntent findAction = PendingIntent.getBroadcast(this, FIND_REQ + device.hashCode(), findAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_find, getString(R.string.proximity_action_find), findAction)); + } + + final Notification notification = builder.build(); + final NotificationManagerCompat nm = NotificationManagerCompat.from(this); + nm.notify(device.getAddress(), NOTIFICATION_ID, notification); + } + + /** + * Creates a notification showing information about a device that got disconnected. + */ + private void createLinklossNotification(final BluetoothDevice device) { + final NotificationCompat.Builder builder = getNotificationBuilder(); + builder.setColor(ContextCompat.getColor(this, R.color.orange)); + + final Uri notificationUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); + builder.setSound(notificationUri, AudioManager.STREAM_ALARM); // make sure the sound is played even in DND mode + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setCategory(NotificationCompat.CATEGORY_ALARM); + builder.setShowWhen(true).setOngoing(false); // an ongoing notification would not be shown on Android Wear + // This notification is to be shown not in a group + + final String name = getDeviceName(device); + builder.setContentTitle(getString(R.string.proximity_notification_linkloss_alert, name)); + builder.setTicker(getString(R.string.proximity_notification_linkloss_alert, name)); + + final Notification notification = builder.build(); + final NotificationManagerCompat nm = NotificationManagerCompat.from(this); + nm.notify(device.getAddress(), NOTIFICATION_ID, notification); + } + + private NotificationCompat.Builder getNotificationBuilder() { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, ProximityActivity.class); + + // Both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed + final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); + + final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setContentIntent(pendingIntent).setAutoCancel(false); + builder.setSmallIcon(R.drawable.ic_stat_notify_proximity); + return builder; + } + /** * Cancels the existing notification. If there is no active notification this method does nothing */ - private void cancelNotification() { + private void cancelNotifications() { final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); nm.cancel(NOTIFICATION_ID); + + final List managedDevices = getManagedDevices(); + for (final BluetoothDevice device : managedDevices) { + nm.cancel(device.getAddress(), NOTIFICATION_ID); + } + } + + /** + * Cancels the existing notification for given device. If there is no active notification this method does nothing + */ + private void cancelNotification(final BluetoothDevice device) { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(device.getAddress(), NOTIFICATION_ID); } private void initializeAlarm() { + mDevicesWithAlarm = new LinkedList<>(); final Uri alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); mRingtoneAlarm = RingtoneManager.getRingtone(this, alarmUri); - - final Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - mRingtoneNotification = RingtoneManager.getRingtone(this, notification); } - private void playNotification() { - mRingtoneNotification.play(); - } + private void playAlarm(final BluetoothDevice device) { + final boolean alarmPlaying = !mDevicesWithAlarm.isEmpty(); + if (!mDevicesWithAlarm.contains(device)) + mDevicesWithAlarm.add(device); - private void playAlarm() { - final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - am.setStreamVolume(AudioManager.STREAM_ALARM, am.getStreamMaxVolume(AudioManager.STREAM_ALARM), AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); - mRingtoneAlarm.play(); - } - - private void stopAlarm() { - mRingtoneAlarm.stop(); - } - - private void startImmediateAlert() { - isImmediateAlertOn = true; - mProximityManager.writeImmediateAlertOn(); - - if (!mBinded) { - createNotification(R.string.proximity_notification_connected_message, 0); + if (!alarmPlaying) { + final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + am.setStreamVolume(AudioManager.STREAM_ALARM, am.getStreamMaxVolume(AudioManager.STREAM_ALARM), AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); + mRingtoneAlarm.play(); } } - private void stopImmediateAlert() { - isImmediateAlertOn = false; - mProximityManager.writeImmediateAlertOff(); - - if (!mBinded) { - createNotification(R.string.proximity_notification_connected_message, 0); + private void stopAlarm(final BluetoothDevice device) { + mDevicesWithAlarm.remove(device); + if (mDevicesWithAlarm.isEmpty()) { + mRingtoneAlarm.stop(); } } + + private String getDeviceName(final BluetoothDevice device) { + String name = device.getName(); + if (TextUtils.isEmpty(name)) + name = getString(R.string.proximity_default_device_name); + return name; + } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/DividerItemDecoration.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/DividerItemDecoration.java new file mode 100644 index 00000000..87453790 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/DividerItemDecoration.java @@ -0,0 +1,111 @@ +/************************************************************************************************************************************************* + * 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.nrftoolbox.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private static final int[] ATTRS = new int[]{ + android.R.attr.listDivider + }; + + public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; + + public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; + + private Drawable mDivider; + + private int mOrientation; + + public DividerItemDecoration(Context context, int orientation) { + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + setOrientation(orientation); + } + + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { + throw new IllegalArgumentException("invalid orientation"); + } + mOrientation = orientation; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + public void drawVertical(Canvas c, RecyclerView parent) { + final int left = parent.getPaddingLeft(); + final int right = parent.getWidth() - parent.getPaddingRight(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int top = child.getBottom() + params.bottomMargin; + final int bottom = top + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + public void drawHorizontal(Canvas c, RecyclerView parent) { + final int top = parent.getPaddingTop(); + final int bottom = parent.getHeight() - parent.getPaddingBottom(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int left = child.getRight() + params.rightMargin; + final int right = left + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_action_bluetooth.png b/app/src/main/res/drawable-hdpi/ic_action_bluetooth.png index a0a84a06..fce18840 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_action_bluetooth.png and b/app/src/main/res/drawable-hdpi/ic_action_bluetooth.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_disconnect.png b/app/src/main/res/drawable-hdpi/ic_action_disconnect.png new file mode 100644 index 00000000..e64801ef Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_disconnect.png differ diff --git a/app/src/main/res/drawable-hdpi/proximity_lock_closed.png b/app/src/main/res/drawable-hdpi/proximity_lock_closed.png deleted file mode 100644 index 65220ea4..00000000 Binary files a/app/src/main/res/drawable-hdpi/proximity_lock_closed.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_bluetooth.png b/app/src/main/res/drawable-xhdpi/ic_action_bluetooth.png index 40451ca6..920f5cae 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_bluetooth.png and b/app/src/main/res/drawable-xhdpi/ic_action_bluetooth.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_disconnect.png b/app/src/main/res/drawable-xhdpi/ic_action_disconnect.png new file mode 100644 index 00000000..44d61456 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_disconnect.png differ diff --git a/app/src/main/res/drawable-xhdpi/proximity_lock_closed.png b/app/src/main/res/drawable-xhdpi/proximity_lock_closed.png deleted file mode 100644 index 010a0508..00000000 Binary files a/app/src/main/res/drawable-xhdpi/proximity_lock_closed.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_bluetooth.png b/app/src/main/res/drawable-xxhdpi/ic_action_bluetooth.png new file mode 100644 index 00000000..860c7586 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_bluetooth.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_disconnect.png b/app/src/main/res/drawable-xxhdpi/ic_action_disconnect.png new file mode 100644 index 00000000..035ff884 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_disconnect.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_bluetooth.png b/app/src/main/res/drawable-xxxhdpi/ic_action_bluetooth.png new file mode 100644 index 00000000..90d8a341 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_bluetooth.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_disconnect.png b/app/src/main/res/drawable-xxxhdpi/ic_action_disconnect.png new file mode 100644 index 00000000..b6508487 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_disconnect.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_battery_alert.png b/app/src/main/res/drawable-xxxhdpi/ic_battery_alert.png new file mode 100644 index 00000000..3d8f9378 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_battery_alert.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_battery_full.png b/app/src/main/res/drawable-xxxhdpi/ic_battery_full.png new file mode 100644 index 00000000..cbf2148d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_battery_full.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_proximity_tag.png b/app/src/main/res/drawable-xxxhdpi/ic_proximity_tag.png new file mode 100644 index 00000000..501500ef Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_proximity_tag.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_find.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_find.png new file mode 100644 index 00000000..82972b4e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_find.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_silent.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_silent.png new file mode 100644 index 00000000..df06b06b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_silent.png differ diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml new file mode 100644 index 00000000..afde3102 --- /dev/null +++ b/app/src/main/res/drawable/ic_battery.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_feature_proximity.xml b/app/src/main/res/layout-land/activity_feature_proximity.xml index 5a5ec15f..bf261e38 100644 --- a/app/src/main/res/layout-land/activity_feature_proximity.xml +++ b/app/src/main/res/layout-land/activity_feature_proximity.xml @@ -48,68 +48,31 @@ android:textSize="32dp" android:textStyle="bold"/> - - - + - - - -