content: /*
   * Copyright 2017, The Android Open Source Project
   *
   * 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 com.example.androidthings.gattserver;
  
  import android.app.Activity;
  import android.bluetooth.BluetoothAdapter;
  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.BluetoothManager;
  import android.bluetooth.BluetoothProfile;
  import android.bluetooth.le.AdvertiseCallback;
  import android.bluetooth.le.AdvertiseData;
  import android.bluetooth.le.AdvertiseSettings;
  import android.bluetooth.le.BluetoothLeAdvertiser;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.pm.PackageManager;
  import android.os.Bundle;
  import android.os.ParcelUuid;
  import android.text.format.DateFormat;
  import android.util.Log;
  import android.view.WindowManager;
  import android.widget.TextView;
  
  import java.util.Arrays;
  import java.util.Date;
  import java.util.HashSet;
  import java.util.Set;
  
  public class GattServerActivity extends Activity {
      private static final String TAG = GattServerActivity.class.getSimpleName();
  
      /* Local UI */
      private TextView mLocalTimeView;
      /* Bluetooth API */
      private BluetoothManager mBluetoothManager;
      private BluetoothGattServer mBluetoothGattServer;
      private BluetoothLeAdvertiser mBluetoothLeAdvertiser;
      /* Collection of notification subscribers */
      private Set<BluetoothDevice> mRegisteredDevices = new HashSet<>();
  
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_server);
  
          mLocalTimeView = (TextView) findViewById(R.id.text_time);
  
          // Devices with a display should not go to sleep
          getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
  
          mBluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
          BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
          // We can't continue without proper Bluetooth support
          if (!checkBluetoothSupport(bluetoothAdapter)) {
              finish();
          }
  
          // Register for system Bluetooth events
          IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
          registerReceiver(mBluetoothReceiver, filter);
          if (!bluetoothAdapter.isEnabled()) {
              Log.d(TAG, "Bluetooth is currently disabled...enabling");
              bluetoothAdapter.enable();
          } else {
              Log.d(TAG, "Bluetooth enabled...starting services");
              startAdvertising();
              startServer();
          }
      }
  
      @Override
      protected void onStart() {
          super.onStart();
          // Register for system clock events
          IntentFilter filter = new IntentFilter();
          filter.addAction(Intent.ACTION_TIME_TICK);
          filter.addAction(Intent.ACTION_TIME_CHANGED);
          filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
          registerReceiver(mTimeReceiver, filter);
      }
  
      @Override
      protected void onStop() {
          super.onStop();
          unregisterReceiver(mTimeReceiver);
      }
  
      @Override
      protected void onDestroy() {
          super.onDestroy();
  
          BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
          if (bluetoothAdapter.isEnabled()) {
              stopServer();
              stopAdvertising();
          }
  
          unregisterReceiver(mBluetoothReceiver);
      }
  
      /**
       * Verify the level of Bluetooth support provided by the hardware.
       * @param bluetoothAdapter System {@link BluetoothAdapter}.
       * @return true if Bluetooth is properly supported, false otherwise.
       */
      private boolean checkBluetoothSupport(BluetoothAdapter bluetoothAdapter) {
  
          if (bluetoothAdapter == null) {
              Log.w(TAG, "Bluetooth is not supported");
              return false;
          }
  
          if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
              Log.w(TAG, "Bluetooth LE is not supported");
              return false;
          }
  
          return true;
      }
  
      /**
       * Listens for system time changes and triggers a notification to
       * Bluetooth subscribers.
       */
      private BroadcastReceiver mTimeReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              byte adjustReason;
              switch (intent.getAction()) {
                  case Intent.ACTION_TIME_CHANGED:
                      adjustReason = TimeProfile.ADJUST_MANUAL;
                      break;
                  case Intent.ACTION_TIMEZONE_CHANGED:
                      adjustReason = TimeProfile.ADJUST_TIMEZONE;
                      break;
                  default:
                  case Intent.ACTION_TIME_TICK:
                      adjustReason = TimeProfile.ADJUST_NONE;
                      break;
              }
              long now = System.currentTimeMillis();
              notifyRegisteredDevices(now, adjustReason);
              updateLocalUi(now);
          }
      };
  
      /**
       * Listens for Bluetooth adapter events to enable/disable
       * advertising and server functionality.
       */
      private BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
  
              switch (state) {
                  case BluetoothAdapter.STATE_ON:
                      startAdvertising();
                      startServer();
                      break;
                  case BluetoothAdapter.STATE_OFF:
                      stopServer();
                      stopAdvertising();
                      break;
                  default:
                      // Do nothing
              }
  
          }
      };
  
      /**
       * Begin advertising over Bluetooth that this device is connectable
       * and supports the Current Time Service.
       */
      private void startAdvertising() {
          BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
          mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
          if (mBluetoothLeAdvertiser == null) {
              Log.w(TAG, "Failed to create advertiser");
              return;
          }
  
          AdvertiseSettings settings = new AdvertiseSettings.Builder()
                  .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
                  .setConnectable(true)
                  .setTimeout(0)
                  .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
                  .build();
  
          AdvertiseData data = new AdvertiseData.Builder()
                  .setIncludeDeviceName(true)
                  .setIncludeTxPowerLevel(false)
                  .addServiceUuid(new ParcelUuid(TimeProfile.TIME_SERVICE))
                  .build();
  
          mBluetoothLeAdvertiser
                  .startAdvertising(settings, data, mAdvertiseCallback);
      }
  
      /**
       * Stop Bluetooth advertisements.
       */
      private void stopAdvertising() {
          if (mBluetoothLeAdvertiser == null) return;
  
          mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
      }
  
      /**
       * Initialize the GATT server instance with the services/characteristics
       * from the Time Profile.
       */
      private void startServer() {
          mBluetoothGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
          if (mBluetoothGattServer == null) {
              Log.w(TAG, "Unable to create GATT server");
              return;
          }
  
          mBluetoothGattServer.addService(TimeProfile.createTimeService());
  
          // Initialize the local UI
          updateLocalUi(System.currentTimeMillis());
      }
  
      /**
       * Shut down the GATT server.
       */
      private void stopServer() {
          if (mBluetoothGattServer == null) return;
  
          mBluetoothGattServer.close();
      }
  
      /**
       * Callback to receive information about the advertisement process.
       */
      private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
          @Override
          public void onStartSuccess(AdvertiseSettings settingsInEffect) {
              Log.i(TAG, "LE Advertise Started.");
          }
  
          @Override
          public void onStartFailure(int errorCode) {
              Log.w(TAG, "LE Advertise Failed: "+errorCode);
          }
      };
  
      /**
       * Send a time service notification to any devices that are subscribed
       * to the characteristic.
       */
      private void notifyRegisteredDevices(long timestamp, byte adjustReason) {
          if (mRegisteredDevices.isEmpty()) {
              Log.i(TAG, "No subscribers registered");
              return;
          }
          byte[] exactTime = TimeProfile.getExactTime(timestamp, adjustReason);
  
          Log.i(TAG, "Sending update to " + mRegisteredDevices.size() + " subscribers");
          for (BluetoothDevice device : mRegisteredDevices) {
              BluetoothGattCharacteristic timeCharacteristic = mBluetoothGattServer
                      .getService(TimeProfile.TIME_SERVICE)
                      .getCharacteristic(TimeProfile.CURRENT_TIME);
              timeCharacteristic.setValue(exactTime);
              mBluetoothGattServer.notifyCharacteristicChanged(device, timeCharacteristic, false);
          }
      }
  
      /**
       * Update graphical UI on devices that support it with the current time.
       */
      private void updateLocalUi(long timestamp) {
          Date date = new Date(timestamp);
          String displayDate = DateFormat.getMediumDateFormat(this).format(date)
                  + "\n"
                  + DateFormat.getTimeFormat(this).format(date);
          mLocalTimeView.setText(displayDate);
      }
  
      /**
       * Callback to handle incoming requests to the GATT server.
       * All read/write requests for characteristics and descriptors are handled here.
       */
      private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
  
          @Override
          public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
              if (newState == BluetoothProfile.STATE_CONNECTED) {
                  Log.i(TAG, "BluetoothDevice CONNECTED: " + device);
              } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                  Log.i(TAG, "BluetoothDevice DISCONNECTED: " + device);
                  //Remove device from any active subscriptions
                  mRegisteredDevices.remove(device);
              }
          }
  
          @Override
          public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
                                                  BluetoothGattCharacteristic characteristic) {
              long now = System.currentTimeMillis();
              if (TimeProfile.CURRENT_TIME.equals(characteristic.getUuid())) {
                  Log.i(TAG, "Read CurrentTime");
                  mBluetoothGattServer.sendResponse(device,
                          requestId,
                          BluetoothGatt.GATT_SUCCESS,
                          0,
                          TimeProfile.getExactTime(now, TimeProfile.ADJUST_NONE));
              } else if (TimeProfile.LOCAL_TIME_INFO.equals(characteristic.getUuid())) {
                  Log.i(TAG, "Read LocalTimeInfo");
                  mBluetoothGattServer.sendResponse(device,
                          requestId,
                          BluetoothGatt.GATT_SUCCESS,
                          0,
                          TimeProfile.getLocalTimeInfo(now));
              } else {
                  // Invalid characteristic
                  Log.w(TAG, "Invalid Characteristic Read: " + characteristic.getUuid());
                  mBluetoothGattServer.sendResponse(device,
                          requestId,
                          BluetoothGatt.GATT_FAILURE,
                          0,
                          null);
              }
          }
  
          @Override
          public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
                                              BluetoothGattDescriptor descriptor) {
              if (TimeProfile.CLIENT_CONFIG.equals(descriptor.getUuid())) {
                  Log.d(TAG, "Config descriptor read");
                  byte[] returnValue;
                  if (mRegisteredDevices.contains(device)) {
                      returnValue = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
                  } else {
                      returnValue = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
                  }
                  mBluetoothGattServer.sendResponse(device,
                          requestId,
                          BluetoothGatt.GATT_FAILURE,
                          0,
                          returnValue);
              } else {
                  Log.w(TAG, "Unknown descriptor read request");
                  mBluetoothGattServer.sendResponse(device,
                          requestId,
                          BluetoothGatt.GATT_FAILURE,
                          0,
                          null);
              }
          }
  
          @Override
          public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
                                               BluetoothGattDescriptor descriptor,
                                               boolean preparedWrite, boolean responseNeeded,
                                               int offset, byte[] value) {
              if (TimeProfile.CLIENT_CONFIG.equals(descriptor.getUuid())) {
                  if (Arrays.equals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) {
                      Log.d(TAG, "Subscribe device to notifications: " + device);
                      mRegisteredDevices.add(device);
                  } else if (Arrays.equals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) {
                      Log.d(TAG, "Unsubscribe device from notifications: " + device);
                      mRegisteredDevices.remove(device);
                  }
  
                  if (responseNeeded) {
                      mBluetoothGattServer.sendResponse(device,
                              requestId,
                              BluetoothGatt.GATT_SUCCESS,
                              0,
                              null);
                  }
              } else {
                  Log.w(TAG, "Unknown descriptor write request");
                  if (responseNeeded) {
                      mBluetoothGattServer.sendResponse(device,
                              requestId,
                              BluetoothGatt.GATT_FAILURE,
                              0,
                              null);
                  }
              }
          }
      };
  }
  