From 672fe9e0d470eaea23a9511aa8db44ea4c6f8b94 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Tue, 25 Apr 2017 19:10:43 +0200 Subject: [PATCH] initial version from BluetoothChat Android Sample --- .gitignore | 9 + .idea/compiler.xml | 22 + .idea/copyright/profiles_settings.xml | 3 + .idea/encodings.xml | 6 + .idea/gradle.xml | 18 + .../animated_vector_drawable_25_0_1.xml | 12 + .idea/libraries/appcompat_v7_25_0_1.xml | 12 + .idea/libraries/cardview_v7_25_0_1.xml | 12 + .idea/libraries/gridlayout_v7_25_0_1.xml | 12 + .../libraries/support_annotations_25_0_1.xml | 11 + .idea/libraries/support_compat_25_0_1.xml | 13 + .idea/libraries/support_core_ui_25_0_1.xml | 13 + .idea/libraries/support_core_utils_25_0_1.xml | 13 + .idea/libraries/support_fragment_25_0_1.xml | 13 + .../libraries/support_media_compat_25_0_1.xml | 13 + .idea/libraries/support_v4_25_0_1.xml | 10 + .../support_vector_drawable_25_0_1.xml | 12 + .idea/markdown-navigator.xml | 70 ++ .../markdown-navigator/profiles_settings.xml | 3 + .idea/misc.xml | 96 +++ .idea/modules.xml | 9 + .idea/runConfigurations.xml | 12 + .idea/vcs.xml | 6 + Application/Application.iml | 123 ++++ Application/build.gradle | 59 ++ Application/src/main/AndroidManifest.xml | 53 ++ .../iconsole/BluetoothChatFragment.java | 403 +++++++++++ .../iconsole/BluetoothChatService.java | 405 +++++++++++ .../java/org/surfsite/iconsole/Constants.java | 35 + .../surfsite/iconsole/DeviceListActivity.java | 217 ++++++ .../java/org/surfsite/iconsole/IConsole.java | 166 +++++ .../org/surfsite/iconsole/MainActivity.java | 111 +++ .../common/activities/SampleActivityBase.java | 52 ++ .../surfsite/iconsole/common/logger/Log.java | 236 +++++++ .../iconsole/common/logger/LogFragment.java | 109 +++ .../iconsole/common/logger/LogNode.java | 39 ++ .../iconsole/common/logger/LogView.java | 145 ++++ .../iconsole/common/logger/LogWrapper.java | 75 ++ .../common/logger/MessageOnlyLogFilter.java | 60 ++ ...tion_device_access_bluetooth_searching.png | Bin 0 -> 1355 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4689 bytes .../src/main/res/drawable-hdpi/tile.9.png | Bin 0 -> 196 bytes ...tion_device_access_bluetooth_searching.png | Bin 0 -> 841 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2834 bytes ...tion_device_access_bluetooth_searching.png | Bin 0 -> 1879 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 6681 bytes ...tion_device_access_bluetooth_searching.png | Bin 0 -> 3083 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 12071 bytes .../main/res/layout-w720dp/activity_main.xml | 73 ++ .../main/res/layout/activity_device_list.xml | 66 ++ .../src/main/res/layout/activity_main.xml | 65 ++ .../src/main/res/layout/device_name.xml | 21 + .../res/layout/fragment_bluetooth_chat.xml | 49 ++ Application/src/main/res/layout/message.xml | 21 + .../src/main/res/menu/bluetooth_chat.xml | 34 + Application/src/main/res/menu/main.xml | 21 + .../res/values-sw600dp/template-dimens.xml | 24 + .../res/values-sw600dp/template-styles.xml | 25 + .../main/res/values-v11/template-styles.xml | 22 + .../src/main/res/values-v21/base-colors.xml | 21 + .../res/values-v21/base-template-styles.xml | 24 + .../src/main/res/values/base-strings.xml | 35 + .../main/res/values/fragmentview_strings.xml | 19 + Application/src/main/res/values/strings.xml | 41 ++ .../src/main/res/values/template-dimens.xml | 32 + .../src/main/res/values/template-styles.xml | 42 ++ BluetoothChat.iml | 19 + LICENSE | 647 ++++++++++++++++++ README.md | 5 + build.gradle | 14 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++++ gradlew.bat | 90 +++ settings.gradle | 4 + 75 files changed, 4272 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/libraries/animated_vector_drawable_25_0_1.xml create mode 100644 .idea/libraries/appcompat_v7_25_0_1.xml create mode 100644 .idea/libraries/cardview_v7_25_0_1.xml create mode 100644 .idea/libraries/gridlayout_v7_25_0_1.xml create mode 100644 .idea/libraries/support_annotations_25_0_1.xml create mode 100644 .idea/libraries/support_compat_25_0_1.xml create mode 100644 .idea/libraries/support_core_ui_25_0_1.xml create mode 100644 .idea/libraries/support_core_utils_25_0_1.xml create mode 100644 .idea/libraries/support_fragment_25_0_1.xml create mode 100644 .idea/libraries/support_media_compat_25_0_1.xml create mode 100644 .idea/libraries/support_v4_25_0_1.xml create mode 100644 .idea/libraries/support_vector_drawable_25_0_1.xml create mode 100644 .idea/markdown-navigator.xml create mode 100644 .idea/markdown-navigator/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 Application/Application.iml create mode 100644 Application/build.gradle create mode 100644 Application/src/main/AndroidManifest.xml create mode 100644 Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/Constants.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/DeviceListActivity.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/IConsole.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/MainActivity.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/common/activities/SampleActivityBase.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/common/logger/Log.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/common/logger/LogFragment.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/common/logger/LogNode.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/common/logger/LogView.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/common/logger/LogWrapper.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/common/logger/MessageOnlyLogFilter.java create mode 100755 Application/src/main/res/drawable-hdpi/ic_action_device_access_bluetooth_searching.png create mode 100644 Application/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 Application/src/main/res/drawable-hdpi/tile.9.png create mode 100755 Application/src/main/res/drawable-mdpi/ic_action_device_access_bluetooth_searching.png create mode 100644 Application/src/main/res/drawable-mdpi/ic_launcher.png create mode 100755 Application/src/main/res/drawable-xhdpi/ic_action_device_access_bluetooth_searching.png create mode 100644 Application/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100755 Application/src/main/res/drawable-xxhdpi/ic_action_device_access_bluetooth_searching.png create mode 100644 Application/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100755 Application/src/main/res/layout-w720dp/activity_main.xml create mode 100644 Application/src/main/res/layout/activity_device_list.xml create mode 100755 Application/src/main/res/layout/activity_main.xml create mode 100644 Application/src/main/res/layout/device_name.xml create mode 100644 Application/src/main/res/layout/fragment_bluetooth_chat.xml create mode 100644 Application/src/main/res/layout/message.xml create mode 100644 Application/src/main/res/menu/bluetooth_chat.xml create mode 100644 Application/src/main/res/menu/main.xml create mode 100644 Application/src/main/res/values-sw600dp/template-dimens.xml create mode 100644 Application/src/main/res/values-sw600dp/template-styles.xml create mode 100644 Application/src/main/res/values-v11/template-styles.xml create mode 100644 Application/src/main/res/values-v21/base-colors.xml create mode 100644 Application/src/main/res/values-v21/base-template-styles.xml create mode 100644 Application/src/main/res/values/base-strings.xml create mode 100755 Application/src/main/res/values/fragmentview_strings.xml create mode 100644 Application/src/main/res/values/strings.xml create mode 100644 Application/src/main/res/values/template-dimens.xml create mode 100644 Application/src/main/res/values/template-styles.xml create mode 100644 BluetoothChat.iml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..7c4fca0 --- /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..773d366 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/libraries/animated_vector_drawable_25_0_1.xml b/.idea/libraries/animated_vector_drawable_25_0_1.xml new file mode 100644 index 0000000..001ea48 --- /dev/null +++ b/.idea/libraries/animated_vector_drawable_25_0_1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/appcompat_v7_25_0_1.xml b/.idea/libraries/appcompat_v7_25_0_1.xml new file mode 100644 index 0000000..eafe803 --- /dev/null +++ b/.idea/libraries/appcompat_v7_25_0_1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/cardview_v7_25_0_1.xml b/.idea/libraries/cardview_v7_25_0_1.xml new file mode 100644 index 0000000..196fa7c --- /dev/null +++ b/.idea/libraries/cardview_v7_25_0_1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/gridlayout_v7_25_0_1.xml b/.idea/libraries/gridlayout_v7_25_0_1.xml new file mode 100644 index 0000000..f992d34 --- /dev/null +++ b/.idea/libraries/gridlayout_v7_25_0_1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_annotations_25_0_1.xml b/.idea/libraries/support_annotations_25_0_1.xml new file mode 100644 index 0000000..ab49259 --- /dev/null +++ b/.idea/libraries/support_annotations_25_0_1.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_compat_25_0_1.xml b/.idea/libraries/support_compat_25_0_1.xml new file mode 100644 index 0000000..4cb0dee --- /dev/null +++ b/.idea/libraries/support_compat_25_0_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_core_ui_25_0_1.xml b/.idea/libraries/support_core_ui_25_0_1.xml new file mode 100644 index 0000000..dc82abb --- /dev/null +++ b/.idea/libraries/support_core_ui_25_0_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_core_utils_25_0_1.xml b/.idea/libraries/support_core_utils_25_0_1.xml new file mode 100644 index 0000000..37d4b9d --- /dev/null +++ b/.idea/libraries/support_core_utils_25_0_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_fragment_25_0_1.xml b/.idea/libraries/support_fragment_25_0_1.xml new file mode 100644 index 0000000..7bab9bc --- /dev/null +++ b/.idea/libraries/support_fragment_25_0_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_media_compat_25_0_1.xml b/.idea/libraries/support_media_compat_25_0_1.xml new file mode 100644 index 0000000..417e443 --- /dev/null +++ b/.idea/libraries/support_media_compat_25_0_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_v4_25_0_1.xml b/.idea/libraries/support_v4_25_0_1.xml new file mode 100644 index 0000000..b2688b5 --- /dev/null +++ b/.idea/libraries/support_v4_25_0_1.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_vector_drawable_25_0_1.xml b/.idea/libraries/support_vector_drawable_25_0_1.xml new file mode 100644 index 0000000..a0a877c --- /dev/null +++ b/.idea/libraries/support_vector_drawable_25_0_1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml new file mode 100644 index 0000000..4fdc309 --- /dev/null +++ b/.idea/markdown-navigator.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml new file mode 100644 index 0000000..57927c5 --- /dev/null +++ b/.idea/markdown-navigator/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..15ae897 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Android API 19 Platform + + + + + + + + 1.8 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4e077ec --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ 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/Application/Application.iml b/Application/Application.iml new file mode 100644 index 0000000..620e43d --- /dev/null +++ b/Application/Application.iml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Application/build.gradle b/Application/build.gradle new file mode 100644 index 0000000..5ec9d77 --- /dev/null +++ b/Application/build.gradle @@ -0,0 +1,59 @@ + +buildscript { + repositories { + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:2.3.1' + } +} + +apply plugin: 'com.android.application' + +repositories { + jcenter() +} + +dependencies { + compile "com.android.support:support-v4:25.0.1" + compile "com.android.support:gridlayout-v7:25.0.1" + compile "com.android.support:cardview-v7:25.0.1" + compile "com.android.support:appcompat-v7:25.0.1" +} + +// The sample build uses multiple directories to +// keep boilerplate and common code separate from +// the main sample code. +List dirs = [ + 'main', // main sample code; look here for the interesting stuff. + 'common', // components that are reused by multiple samples + 'template'] // boilerplate code that is generated by the sample template process + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + defaultConfig { + minSdkVersion 11 + targetSdkVersion 25 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + + sourceSets { + main { + dirs.each { dir -> + java.srcDirs "src/${dir}/java" + res.srcDirs "src/${dir}/res" + } + } + androidTest.setRoot('tests') + androidTest.java.srcDirs = ['tests/src'] + + } + +} diff --git a/Application/src/main/AndroidManifest.xml b/Application/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1322b10 --- /dev/null +++ b/Application/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java new file mode 100644 index 0000000..54f484d --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2014 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 org.surfsite.iconsole; + +import android.app.ActionBar; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.view.KeyEvent; +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.view.inputmethod.EditorInfo; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.bluetoothchat.R; +import org.surfsite.iconsole.common.logger.Log; + +/** + * This fragment controls Bluetooth to communicate with other devices. + */ +public class BluetoothChatFragment extends Fragment { + + private static final String TAG = "BluetoothChatFragment"; + + // Intent request codes + private static final int REQUEST_CONNECT_DEVICE_SECURE = 1; + private static final int REQUEST_CONNECT_DEVICE_INSECURE = 2; + private static final int REQUEST_ENABLE_BT = 3; + + // Layout Views + private ListView mConversationView; + private EditText mOutEditText; + private Button mSendButton; + + /** + * Name of the connected device + */ + private String mConnectedDeviceName = null; + + /** + * Array adapter for the conversation thread + */ + private ArrayAdapter mConversationArrayAdapter; + + /** + * String buffer for outgoing messages + */ + private StringBuffer mOutStringBuffer; + + /** + * Local Bluetooth adapter + */ + private BluetoothAdapter mBluetoothAdapter = null; + + /** + * Member object for the chat services + */ + private BluetoothChatService mChatService = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + // Get local Bluetooth adapter + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + // If the adapter is null, then Bluetooth is not supported + if (mBluetoothAdapter == null) { + FragmentActivity activity = getActivity(); + Toast.makeText(activity, "Bluetooth is not available", Toast.LENGTH_LONG).show(); + activity.finish(); + } + } + + + @Override + public void onStart() { + super.onStart(); + // If BT is not on, request that it be enabled. + // setupChat() will then be called during onActivityResult + if (!mBluetoothAdapter.isEnabled()) { + Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableIntent, REQUEST_ENABLE_BT); + // Otherwise, setup the chat session + } else if (mChatService == null) { + setupChat(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mChatService != null) { + mChatService.stop(); + } + } + + @Override + public void onResume() { + super.onResume(); + + // Performing this check in onResume() covers the case in which BT was + // not enabled during onStart(), so we were paused to enable it... + // onResume() will be called when ACTION_REQUEST_ENABLE activity returns. + if (mChatService != null) { + // Only if the state is STATE_NONE, do we know that we haven't started already + if (mChatService.getState() == BluetoothChatService.STATE_NONE) { + // Start the Bluetooth chat services + mChatService.start(); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_bluetooth_chat, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + mConversationView = (ListView) view.findViewById(R.id.in); + mOutEditText = (EditText) view.findViewById(R.id.edit_text_out); + mSendButton = (Button) view.findViewById(R.id.button_send); + } + + /** + * Set up the UI and background operations for chat. + */ + private void setupChat() { + Log.d(TAG, "setupChat()"); + + // Initialize the array adapter for the conversation thread + mConversationArrayAdapter = new ArrayAdapter(getActivity(), R.layout.message); + + mConversationView.setAdapter(mConversationArrayAdapter); + + // Initialize the compose field with a listener for the return key + mOutEditText.setOnEditorActionListener(mWriteListener); + + // Initialize the send button with a listener that for click events + mSendButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // Send a message using content of the edit text widget + View view = getView(); + if (null != view) { + TextView textView = (TextView) view.findViewById(R.id.edit_text_out); + String message = textView.getText().toString(); + sendMessage(message); + } + } + }); + + // Initialize the BluetoothChatService to perform bluetooth connections + mChatService = new BluetoothChatService(getActivity(), mHandler); + + // Initialize the buffer for outgoing messages + mOutStringBuffer = new StringBuffer(""); + } + + /** + * Makes this device discoverable for 300 seconds (5 minutes). + */ + private void ensureDiscoverable() { + if (mBluetoothAdapter.getScanMode() != + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); + startActivity(discoverableIntent); + } + } + + /** + * Sends a message. + * + * @param message A string of text to send. + */ + private void sendMessage(String message) { + // Check that we're actually connected before trying anything + if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) { + Toast.makeText(getActivity(), R.string.not_connected, Toast.LENGTH_SHORT).show(); + return; + } + + // Check that there's actually something to send + if (message.length() > 0) { + // Get the message bytes and tell the BluetoothChatService to write + byte[] send = message.getBytes(); + mChatService.write(send); + + // Reset out string buffer to zero and clear the edit text field + mOutStringBuffer.setLength(0); + mOutEditText.setText(mOutStringBuffer); + } + } + + /** + * The action listener for the EditText widget, to listen for the return key + */ + private TextView.OnEditorActionListener mWriteListener + = new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView view, int actionId, KeyEvent event) { + // If the action is a key-up event on the return key, send the message + if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) { + String message = view.getText().toString(); + sendMessage(message); + } + return true; + } + }; + + /** + * Updates the status on the action bar. + * + * @param resId a string resource ID + */ + private void setStatus(int resId) { + FragmentActivity activity = getActivity(); + if (null == activity) { + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (null == actionBar) { + return; + } + actionBar.setSubtitle(resId); + } + + /** + * Updates the status on the action bar. + * + * @param subTitle status + */ + private void setStatus(CharSequence subTitle) { + FragmentActivity activity = getActivity(); + if (null == activity) { + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (null == actionBar) { + return; + } + actionBar.setSubtitle(subTitle); + } + + /** + * The Handler that gets information back from the BluetoothChatService + */ + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + FragmentActivity activity = getActivity(); + switch (msg.what) { + case Constants.MESSAGE_STATE_CHANGE: + switch (msg.arg1) { + case BluetoothChatService.STATE_CONNECTED: + setStatus(getString(R.string.title_connected_to, mConnectedDeviceName)); + mConversationArrayAdapter.clear(); + break; + case BluetoothChatService.STATE_CONNECTING: + setStatus(R.string.title_connecting); + break; + case BluetoothChatService.STATE_LISTEN: + case BluetoothChatService.STATE_NONE: + setStatus(R.string.title_not_connected); + break; + } + break; + case Constants.MESSAGE_WRITE: + byte[] writeBuf = (byte[]) msg.obj; + // construct a string from the buffer + String writeMessage = new String(writeBuf); + mConversationArrayAdapter.add("Me: " + writeMessage); + break; + case Constants.MESSAGE_READ: + byte[] readBuf = (byte[]) msg.obj; + // construct a string from the valid bytes in the buffer + String readMessage = new String(readBuf, 0, msg.arg1); + mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage); + break; + case Constants.MESSAGE_DEVICE_NAME: + // save the connected device's name + mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); + if (null != activity) { + Toast.makeText(activity, "Connected to " + + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); + } + break; + case Constants.MESSAGE_TOAST: + if (null != activity) { + Toast.makeText(activity, msg.getData().getString(Constants.TOAST), + Toast.LENGTH_SHORT).show(); + } + break; + } + } + }; + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CONNECT_DEVICE_SECURE: + // When DeviceListActivity returns with a device to connect + if (resultCode == Activity.RESULT_OK) { + connectDevice(data, true); + } + break; + case REQUEST_CONNECT_DEVICE_INSECURE: + // When DeviceListActivity returns with a device to connect + if (resultCode == Activity.RESULT_OK) { + connectDevice(data, false); + } + break; + case REQUEST_ENABLE_BT: + // When the request to enable Bluetooth returns + if (resultCode == Activity.RESULT_OK) { + // Bluetooth is now enabled, so set up a chat session + setupChat(); + } else { + // User did not enable Bluetooth or an error occurred + Log.d(TAG, "BT not enabled"); + Toast.makeText(getActivity(), R.string.bt_not_enabled_leaving, + Toast.LENGTH_SHORT).show(); + getActivity().finish(); + } + } + } + + /** + * Establish connection with other device + * + * @param data An {@link Intent} with {@link DeviceListActivity#EXTRA_DEVICE_ADDRESS} extra. + * @param secure Socket Security type - Secure (true) , Insecure (false) + */ + private void connectDevice(Intent data, boolean secure) { + // Get the device MAC address + String address = data.getExtras() + .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS); + // Get the BluetoothDevice object + BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); + // Attempt to connect to the device + mChatService.connect(device); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.bluetooth_chat, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.secure_connect_scan: { + // Launch the DeviceListActivity to see devices and do scan + Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class); + startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_SECURE); + return true; + } + case R.id.insecure_connect_scan: { + // Launch the DeviceListActivity to see devices and do scan + Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class); + startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_INSECURE); + return true; + } + case R.id.discoverable: { + // Ensure this device is discoverable by others + ensureDiscoverable(); + return true; + } + } + return false; + } + +} diff --git a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java new file mode 100644 index 0000000..90258a5 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2014 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 org.surfsite.iconsole; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; + +import org.surfsite.iconsole.common.logger.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.UUID; + +/** + * This class does all the work for setting up and managing Bluetooth + * connections with other devices. It has a thread that listens for + * incoming connections, a thread for connecting with a device, and a + * thread for performing data transmissions when connected. + */ +public class BluetoothChatService { + // Debugging + private static final String TAG = "BluetoothChatService"; + // Name for the SDP record when creating server socket + private static final String NAME_SECURE = "BluetoothChatSecure"; + private static final String NAME_INSECURE = "BluetoothChatInsecure"; + + // Unique UUID for this application + private static final UUID SERIAL_PORT_CLASS = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); + // Member fields + private final BluetoothAdapter mAdapter; + private final Handler mHandler; + + private ConnectThread mConnectThread; + private ConnectedThread mConnectedThread; + private int mState; + private int mNewState; + + // Constants that indicate the current connection state + public static final int STATE_NONE = 0; // we're doing nothing + public static final int STATE_LISTEN = 1; // now listening for incoming connections + public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection + public static final int STATE_CONNECTED = 3; // now connected to a remote device + + /** + * Constructor. Prepares a new BluetoothChat session. + * + * @param context The UI Activity Context + * @param handler A Handler to send messages back to the UI Activity + */ + public BluetoothChatService(Context context, Handler handler) { + mAdapter = BluetoothAdapter.getDefaultAdapter(); + mState = STATE_NONE; + mNewState = mState; + mHandler = handler; + } + + + /** + * Update UI title according to the current state of the chat connection + */ + private synchronized void updateUserInterfaceTitle() { + mState = getState(); + Log.d(TAG, "updateUserInterfaceTitle() " + mNewState + " -> " + mState); + mNewState = mState; + + // Give the new state to the Handler so the UI Activity can update + mHandler.obtainMessage(Constants.MESSAGE_STATE_CHANGE, mNewState, -1).sendToTarget(); + } + + /** + * Return the current connection state. + */ + public synchronized int getState() { + return mState; + } + + /** + * Start the chat service. Specifically start AcceptThread to begin a + * session in listening (server) mode. Called by the Activity onResume() + */ + public synchronized void start() { + Log.d(TAG, "start"); + + // Cancel any thread attempting to make a connection + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + // Update UI title + updateUserInterfaceTitle(); + } + + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param device The BluetoothDevice to connect + */ + public synchronized void connect(BluetoothDevice device) { + Log.d(TAG, "connect to: " + device); + + // Cancel any thread attempting to make a connection + if (mState == STATE_CONNECTING) { + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + // Start the thread to connect with the given device + mConnectThread = new ConnectThread(device); + mConnectThread.start(); + // Update UI title + updateUserInterfaceTitle(); + } + + /** + * Start the ConnectedThread to begin managing a Bluetooth connection + * + * @param socket The BluetoothSocket on which the connection was made + * @param device The BluetoothDevice that has been connected + */ + public synchronized void connected(BluetoothSocket socket, BluetoothDevice + device, final String socketType) { + Log.d(TAG, "connected, Socket Type:" + socketType); + + // Cancel the thread that completed the connection + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + // Start the thread to manage the connection and perform transmissions + mConnectedThread = new ConnectedThread(socket, socketType); + mConnectedThread.start(); + + // Send the name of the connected device back to the UI Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_DEVICE_NAME); + Bundle bundle = new Bundle(); + bundle.putString(Constants.DEVICE_NAME, device.getName()); + msg.setData(bundle); + mHandler.sendMessage(msg); + // Update UI title + updateUserInterfaceTitle(); + } + + /** + * Stop all threads + */ + public synchronized void stop() { + Log.d(TAG, "stop"); + + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + mState = STATE_NONE; + // Update UI title + updateUserInterfaceTitle(); + } + + /** + * Write to the ConnectedThread in an unsynchronized manner + * + * @param out The bytes to write + * @see ConnectedThread#write(byte[]) + */ + public void write(byte[] out) { + } + + /** + * Indicate that the connection attempt failed and notify the UI Activity. + */ + private void connectionFailed() { + // Send a failure message back to the Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST); + Bundle bundle = new Bundle(); + bundle.putString(Constants.TOAST, "Unable to connect device"); + msg.setData(bundle); + mHandler.sendMessage(msg); + + mState = STATE_NONE; + // Update UI title + updateUserInterfaceTitle(); + + // Start the service over to restart listening mode + BluetoothChatService.this.start(); + } + + /** + * Indicate that the connection was lost and notify the UI Activity. + */ + private void connectionLost() { + // Send a failure message back to the Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST); + Bundle bundle = new Bundle(); + bundle.putString(Constants.TOAST, "Device connection was lost"); + msg.setData(bundle); + mHandler.sendMessage(msg); + + mState = STATE_NONE; + // Update UI title + updateUserInterfaceTitle(); + + // Start the service over to restart listening mode + BluetoothChatService.this.start(); + } + + /** + * This thread runs while attempting to make an outgoing connection + * with a device. It runs straight through; the connection either + * succeeds or fails. + */ + private class ConnectThread extends Thread { + private final BluetoothSocket mmSocket; + private final BluetoothDevice mmDevice; + private String mSocketType; + + public ConnectThread(BluetoothDevice device) { + mmDevice = device; + BluetoothSocket tmp = null; + + // Get a BluetoothSocket for a connection with the + // given BluetoothDevice + try { + tmp = device.createRfcommSocketToServiceRecord(SERIAL_PORT_CLASS); + } catch (IOException e) { + Log.e(TAG, "Socket Type: create() failed", e); + } + mmSocket = tmp; + mState = STATE_CONNECTING; + } + + public void run() { + Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType); + setName("ConnectThread" + mSocketType); + + // Always cancel discovery because it will slow down a connection + mAdapter.cancelDiscovery(); + + // Make a connection to the BluetoothSocket + try { + // This is a blocking call and will only return on a + // successful connection or an exception + mmSocket.connect(); + } catch (IOException e) { + // Close the socket + try { + mmSocket.close(); + } catch (IOException e2) { + Log.e(TAG, "unable to close() " + mSocketType + + " socket during connection failure", e2); + } + connectionFailed(); + return; + } + + // Reset the ConnectThread because we're done + synchronized (BluetoothChatService.this) { + mConnectThread = null; + } + + // Start the connected thread + connected(mmSocket, mmDevice, mSocketType); + } + + public void cancel() { + try { + mmSocket.close(); + } catch (IOException e) { + Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e); + } + } + } + + /** + * This thread runs during a connection with a remote device. + * It handles all incoming and outgoing transmissions. + */ + private class ConnectedThread extends Thread { + private final BluetoothSocket mmSocket; + private final InputStream mmInStream; + private final OutputStream mmOutStream; + + public ConnectedThread(BluetoothSocket socket, String socketType) { + Log.d(TAG, "create ConnectedThread: " + socketType); + mmSocket = socket; + InputStream tmpIn = null; + OutputStream tmpOut = null; + + // Get the BluetoothSocket input and output streams + try { + tmpIn = socket.getInputStream(); + tmpOut = socket.getOutputStream(); + } catch (IOException e) { + Log.e(TAG, "temp sockets not created", e); + } + + mmInStream = tmpIn; + mmOutStream = tmpOut; + mState = STATE_CONNECTED; + } + + public void run() { + Log.i(TAG, "BEGIN mConnectedThread"); + byte[] buffer = new byte[5]; + int bytes; + int i = 0; + + // Keep listening to the InputStream while connected + while (mState == STATE_CONNECTED && i < 100) { + try { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + /* pass */; + } + i++; + write(IConsole.PING); + // Read from the InputStream + bytes = mmInStream.read(buffer); + if (bytes > 0) { + String hexbuf = IConsole.byteArrayToHex(Arrays.copyOfRange(buffer, 0, bytes)) + '\n'; + + // Send the obtained bytes to the UI Activity + mHandler.obtainMessage(Constants.MESSAGE_READ, hexbuf.length(), -1, hexbuf.getBytes()) + .sendToTarget(); + } + } catch (IOException e) { + Log.e(TAG, "disconnected", e); + connectionLost(); + break; + } + } + } + + + /* + public void write(byte[] buffer) { + try { + mmOutStream.write(buffer); + String hexbuf = IConsole.byteArrayToHex(buffer) + '\n'; + + // Share the sent message back to the UI Activity + mHandler.obtainMessage(Constants.MESSAGE_WRITE, -1, -1, hexbuf.getBytes()) + .sendToTarget(); + } catch (IOException e) { + Log.e(TAG, "Exception during write", e); + } + } + */ + + public void cancel() { + try { + mmSocket.close(); + } catch (IOException e) { + Log.e(TAG, "close() of connect socket failed", e); + } + } + } +} diff --git a/Application/src/main/java/org/surfsite/iconsole/Constants.java b/Application/src/main/java/org/surfsite/iconsole/Constants.java new file mode 100644 index 0000000..138cdd1 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/Constants.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 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 org.surfsite.iconsole; + +/** + * Defines several constants used between {@link BluetoothChatService} and the UI. + */ +public interface Constants { + + // Message types sent from the BluetoothChatService Handler + public static final int MESSAGE_STATE_CHANGE = 1; + public static final int MESSAGE_READ = 2; + public static final int MESSAGE_WRITE = 3; + public static final int MESSAGE_DEVICE_NAME = 4; + public static final int MESSAGE_TOAST = 5; + + // Key names received from the BluetoothChatService Handler + public static final String DEVICE_NAME = "device_name"; + public static final String TOAST = "toast"; + +} diff --git a/Application/src/main/java/org/surfsite/iconsole/DeviceListActivity.java b/Application/src/main/java/org/surfsite/iconsole/DeviceListActivity.java new file mode 100644 index 0000000..a528b03 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/DeviceListActivity.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2014 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 org.surfsite.iconsole; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; + +import com.example.android.bluetoothchat.R; +import org.surfsite.iconsole.common.logger.Log; + +import java.util.Set; + +/** + * This Activity appears as a dialog. It lists any paired devices and + * devices detected in the area after discovery. When a device is chosen + * by the user, the MAC address of the device is sent back to the parent + * Activity in the result Intent. + */ +public class DeviceListActivity extends Activity { + + /** + * Tag for Log + */ + private static final String TAG = "DeviceListActivity"; + + /** + * Return Intent extra + */ + public static String EXTRA_DEVICE_ADDRESS = "device_address"; + + /** + * Member fields + */ + private BluetoothAdapter mBtAdapter; + + /** + * Newly discovered devices + */ + private ArrayAdapter mNewDevicesArrayAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.activity_device_list); + + // Set result CANCELED in case the user backs out + setResult(Activity.RESULT_CANCELED); + + // Initialize the button to perform device discovery + Button scanButton = (Button) findViewById(R.id.button_scan); + scanButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + doDiscovery(); + v.setVisibility(View.GONE); + } + }); + + // Initialize array adapters. One for already paired devices and + // one for newly discovered devices + ArrayAdapter pairedDevicesArrayAdapter = + new ArrayAdapter(this, R.layout.device_name); + mNewDevicesArrayAdapter = new ArrayAdapter(this, R.layout.device_name); + + // Find and set up the ListView for paired devices + ListView pairedListView = (ListView) findViewById(R.id.paired_devices); + pairedListView.setAdapter(pairedDevicesArrayAdapter); + pairedListView.setOnItemClickListener(mDeviceClickListener); + + // Find and set up the ListView for newly discovered devices + ListView newDevicesListView = (ListView) findViewById(R.id.new_devices); + newDevicesListView.setAdapter(mNewDevicesArrayAdapter); + newDevicesListView.setOnItemClickListener(mDeviceClickListener); + + // Register for broadcasts when a device is discovered + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + this.registerReceiver(mReceiver, filter); + + // Register for broadcasts when discovery has finished + filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + this.registerReceiver(mReceiver, filter); + + // Get the local Bluetooth adapter + mBtAdapter = BluetoothAdapter.getDefaultAdapter(); + + // Get a set of currently paired devices + Set pairedDevices = mBtAdapter.getBondedDevices(); + + // If there are paired devices, add each one to the ArrayAdapter + if (pairedDevices.size() > 0) { + findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE); + for (BluetoothDevice device : pairedDevices) { + pairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); + } + } else { + String noDevices = getResources().getText(R.string.none_paired).toString(); + pairedDevicesArrayAdapter.add(noDevices); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + // Make sure we're not doing discovery anymore + if (mBtAdapter != null) { + mBtAdapter.cancelDiscovery(); + } + + // Unregister broadcast listeners + this.unregisterReceiver(mReceiver); + } + + /** + * Start device discover with the BluetoothAdapter + */ + private void doDiscovery() { + Log.d(TAG, "doDiscovery()"); + + // Indicate scanning in the title + setProgressBarIndeterminateVisibility(true); + setTitle(R.string.scanning); + + // Turn on sub-title for new devices + findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); + + // If we're already discovering, stop it + if (mBtAdapter.isDiscovering()) { + mBtAdapter.cancelDiscovery(); + } + + // Request discover from BluetoothAdapter + mBtAdapter.startDiscovery(); + } + + /** + * The on-click listener for all devices in the ListViews + */ + private AdapterView.OnItemClickListener mDeviceClickListener + = new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView av, View v, int arg2, long arg3) { + // Cancel discovery because it's costly and we're about to connect + mBtAdapter.cancelDiscovery(); + + // Get the device MAC address, which is the last 17 chars in the View + String info = ((TextView) v).getText().toString(); + String address = info.substring(info.length() - 17); + + // Create the result Intent and include the MAC address + Intent intent = new Intent(); + intent.putExtra(EXTRA_DEVICE_ADDRESS, address); + + // Set result and finish this Activity + setResult(Activity.RESULT_OK, intent); + finish(); + } + }; + + /** + * The BroadcastReceiver that listens for discovered devices and changes the title when + * discovery is finished + */ + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + // When discovery finds a device + if (BluetoothDevice.ACTION_FOUND.equals(action)) { + // Get the BluetoothDevice object from the Intent + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + // If it's already paired, skip it, because it's been listed already + if (device.getBondState() != BluetoothDevice.BOND_BONDED) { + mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); + } + // When discovery is finished, change the Activity title + } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { + setProgressBarIndeterminateVisibility(false); + setTitle(R.string.select_device); + if (mNewDevicesArrayAdapter.getCount() == 0) { + String noDevices = getResources().getText(R.string.none_found).toString(); + mNewDevicesArrayAdapter.add(noDevices); + } + } + } + }; + +} diff --git a/Application/src/main/java/org/surfsite/iconsole/IConsole.java b/Application/src/main/java/org/surfsite/iconsole/IConsole.java new file mode 100644 index 0000000..de67f26 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/IConsole.java @@ -0,0 +1,166 @@ +package org.surfsite.iconsole; + +/** + * Created by harald on 25.04.17. + */ + +public class IConsole { + public static final byte[] PING = { (byte) 0xf0, (byte) 0xa0, (byte) 0x01, (byte) 0x01, (byte) 0x92 }; + /* + INIT_A0 = struct.pack('BBBBB', 0xf0, 0xa0, 0x02, 0x02, 0x94) + PING = struct.pack('BBBBB', 0xf0, 0xa0, 0x01, 0x01, 0x92) + PONG = struct.pack('BBBBB', 0xf0, 0xb0, 0x01, 0x01, 0xa2) + STATUS = struct.pack('BBBBB', 0xf0, 0xa1, 0x01, 0x01, 0x93) + INIT_A3 = struct.pack('BBBBBB', 0xf0, 0xa3, 0x01, 0x01, 0x01, 0x96) + INIT_A4 = struct.pack('BBBBBBBBBBBBBBB', 0xf0, 0xa4, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xa0) + START = struct.pack('BBBBBB', 0xf0, 0xa5, 0x01, 0x01, 0x02, 0x99) + STOP = struct.pack('BBBBBB', 0xf0, 0xa5, 0x01, 0x01, 0x04, 0x9b) + READ = struct.pack('BBBBB', 0xf0, 0xa2, 0x01, 0x01, 0x94) +*/ + + /* + def __init__(self, got): + gota = struct.unpack('BBBBBBBBBBBBBBBBBBBBB', got) + self.time_str = "%02d:%02d:%02d:%02d" % (gota[2]-1, gota[3]-1, gota[4]-1, gota[5]-1) + self.speed = ((100*(gota[6]-1) + gota[7] -1) / 10.0) + self.speed_str = "V: % 3.1f km/h" % self.speed + self.rpm = ((100*(gota[8]-1) + gota[9] -1)) + self.rpm_str = "% 3d RPM" % self.rpm + self.distance = ((100*(gota[10]-1) + gota[11] -1) / 10.0) + self.distance_str = "D: % 3.1f km" % self.distance + self.calories = ((100*(gota[12]-1) + gota[13] -1)) + self.calories_str = "% 3d kcal" % self.calories + self.hf = ((100*(gota[14]-1) + gota[15] -1)) + self.hf_str = "HF % 3d" % self.hf + self.power = ((100*(gota[16]-1) + gota[17] -1) / 10.0) + self.power_str = "% 3.1f W" % self.power + self.lvl = gota[18] -1 + self.lvl_str = "L: %d" % self.lvl + */ + + /* + def send_ack(packet, expect=None, plen=0): + if expect == None: + expect = 0xb0 | (ord(packet[1]) & 0xF) + + if plen == 0: + plen = len(packet) + + got = None + while got == None: + sleep(0.1) + sock.sendall(packet) + i = 0 + while got == None and i < 6: + i+=1 + sleep(0.1) + got = sock.recv(plen) + if len(got) == plen: + #print "<-" + hexlify(got) + pass + else: + if len(got) > 0: + #print "Got len == %d" % len(got) + pass + got = None + + if got and len(got) >= 3 and got[0] == packet[0] and ord(got[1]) == expect: + break + got = None + #print "---> Retransmit" + return got + +def send_level(lvl): + packet = struct.pack('BBBBBB', 0xf0, 0xa6, 0x01, 0x01, lvl+1, (0xf0+0xa6+3+lvl) & 0xFF) + got = send_ack(packet) + return got + + */ + +/* + send_ack(PING) + prints(win, "ping done") + + send_ack(INIT_A0, expect=0xb7, plen=6) + prints(win, "A0 done") + + for i in range(0, 5): + send_ack(PING) + prints(win, "ping done") + + send_ack(STATUS, plen=6) + prints(win, "status done") + + send_ack(PING) + prints(win, "ping done") + + send_ack(INIT_A3) + prints(win, "A3 done") + + send_ack(INIT_A4) + prints(win, "A4 done") + + send_ack(START) + prints(win, "START done") + + level = 1 + + while True: + sleep(0.25) + while True: + key = win.getch() + if key == ord('q'): + return + elif key == ord('a') or key == curses.KEY_UP or key == curses.KEY_RIGHT: + if level < 31: + level += 1 + prints(win, "Level: %d" % level) + send_level(level) + + elif key == ord('y') or key == curses.KEY_DOWN or key == curses.KEY_LEFT: + if level > 1: + level -= 1 + prints(win, "Level: %d" % level) + send_level(level) + elif key == -1: + break + + got = send_ack(READ, plen=21) + if len(got) == 21: + ic = IConsole(got) + power_meter.update(power = ic.power, cadence = ic.rpm) + speed.update(ic.speed) + win.addstr(0,0, "%s - %s - %s - %s - %s - %s - %s - %s" % (ic.time_str, + ic.speed_str, + ic.rpm_str, + ic.distance_str, + ic.calories_str, + ic.hf_str, + ic.power_str, + ic.lvl_str)) + + */ + +/* + send_ack(STOP) + send_ack(PING) + + */ + + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + public static String byteArrayToHex(byte[] a) { + StringBuilder sb = new StringBuilder(a.length * 2); + for(byte b: a) + sb.append(String.format("%02x", b)); + return sb.toString(); + } +} diff --git a/Application/src/main/java/org/surfsite/iconsole/MainActivity.java b/Application/src/main/java/org/surfsite/iconsole/MainActivity.java new file mode 100644 index 0000000..2f3148a --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/MainActivity.java @@ -0,0 +1,111 @@ +/* +* Copyright 2013 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 org.surfsite.iconsole; + +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewAnimator; + +import com.example.android.bluetoothchat.R; +import org.surfsite.iconsole.common.activities.SampleActivityBase; +import org.surfsite.iconsole.common.logger.Log; +import org.surfsite.iconsole.common.logger.LogFragment; +import org.surfsite.iconsole.common.logger.LogWrapper; +import org.surfsite.iconsole.common.logger.MessageOnlyLogFilter; + +/** + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + *

+ * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. + */ +public class MainActivity extends SampleActivityBase { + + public static final String TAG = "MainActivity"; + + // Whether the Log Fragment is currently shown + private boolean mLogShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (savedInstanceState == null) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + BluetoothChatFragment fragment = new BluetoothChatFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem logToggle = menu.findItem(R.id.menu_toggle_log); + logToggle.setVisible(findViewById(R.id.sample_output) instanceof ViewAnimator); + logToggle.setTitle(mLogShown ? R.string.sample_hide_log : R.string.sample_show_log); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_toggle_log: + mLogShown = !mLogShown; + ViewAnimator output = (ViewAnimator) findViewById(R.id.sample_output); + if (mLogShown) { + output.setDisplayedChild(1); + } else { + output.setDisplayedChild(0); + } + supportInvalidateOptionsMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Create a chain of targets that will receive log data */ + @Override + public void initializeLogging() { + // Wraps Android's native log framework. + LogWrapper logWrapper = new LogWrapper(); + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + Log.setLogNode(logWrapper); + + // Filter strips out everything except the message text. + MessageOnlyLogFilter msgFilter = new MessageOnlyLogFilter(); + logWrapper.setNext(msgFilter); + + // On screen logging via a fragment with a TextView. + LogFragment logFragment = (LogFragment) getSupportFragmentManager() + .findFragmentById(R.id.log_fragment); + msgFilter.setNext(logFragment.getLogView()); + + Log.i(TAG, "Ready"); + } +} diff --git a/Application/src/main/java/org/surfsite/iconsole/common/activities/SampleActivityBase.java b/Application/src/main/java/org/surfsite/iconsole/common/activities/SampleActivityBase.java new file mode 100644 index 0000000..80d3007 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/common/activities/SampleActivityBase.java @@ -0,0 +1,52 @@ +/* +* Copyright 2013 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 org.surfsite.iconsole.common.activities; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +import org.surfsite.iconsole.common.logger.Log; +import org.surfsite.iconsole.common.logger.LogWrapper; + +/** + * Base launcher activity, to handle most of the common plumbing for samples. + */ +public class SampleActivityBase extends FragmentActivity { + + public static final String TAG = "SampleActivityBase"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onStart() { + super.onStart(); + initializeLogging(); + } + + /** Set up targets to receive log data */ + public void initializeLogging() { + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + // Wraps Android's native log framework + LogWrapper logWrapper = new LogWrapper(); + Log.setLogNode(logWrapper); + + Log.i(TAG, "Ready"); + } +} diff --git a/Application/src/main/java/org/surfsite/iconsole/common/logger/Log.java b/Application/src/main/java/org/surfsite/iconsole/common/logger/Log.java new file mode 100644 index 0000000..6f63c2c --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/common/logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 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 org.surfsite.iconsole.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + *

When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.

+ */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/Application/src/main/java/org/surfsite/iconsole/common/logger/LogFragment.java b/Application/src/main/java/org/surfsite/iconsole/common/logger/LogFragment.java new file mode 100644 index 0000000..09ec135 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/common/logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 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. +*/ +/* + * Copyright 2013 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 org.surfsite.iconsole.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.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) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +} \ No newline at end of file diff --git a/Application/src/main/java/org/surfsite/iconsole/common/logger/LogNode.java b/Application/src/main/java/org/surfsite/iconsole/common/logger/LogNode.java new file mode 100644 index 0000000..e11feb2 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/common/logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 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 org.surfsite.iconsole.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/Application/src/main/java/org/surfsite/iconsole/common/logger/LogView.java b/Application/src/main/java/org/surfsite/iconsole/common/logger/LogView.java new file mode 100644 index 0000000..85b773d --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/common/logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 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 org.surfsite.iconsole.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/Application/src/main/java/org/surfsite/iconsole/common/logger/LogWrapper.java b/Application/src/main/java/org/surfsite/iconsole/common/logger/LogWrapper.java new file mode 100644 index 0000000..dd578f7 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/common/logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012 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 org.surfsite.iconsole.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/Application/src/main/java/org/surfsite/iconsole/common/logger/MessageOnlyLogFilter.java b/Application/src/main/java/org/surfsite/iconsole/common/logger/MessageOnlyLogFilter.java new file mode 100644 index 0000000..92d9eb3 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/common/logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 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 org.surfsite.iconsole.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/Application/src/main/res/drawable-hdpi/ic_action_device_access_bluetooth_searching.png b/Application/src/main/res/drawable-hdpi/ic_action_device_access_bluetooth_searching.png new file mode 100755 index 0000000000000000000000000000000000000000..fc0491e637c99e30cae73e0bac1056707f8d1874 GIT binary patch literal 1355 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 Tjtm-<00000NkvXXu0mjf6eM=r literal 0 HcmV?d00001 diff --git a/Application/src/main/res/drawable-mdpi/ic_launcher.png b/Application/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..1c51ccd7c91801c4c5b5bbbbb7ee1484952cfb11 GIT binary patch literal 2834 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 literal 0 HcmV?d00001 diff --git a/Application/src/main/res/drawable-xhdpi/ic_action_device_access_bluetooth_searching.png b/Application/src/main/res/drawable-xhdpi/ic_action_device_access_bluetooth_searching.png new file mode 100755 index 0000000000000000000000000000000000000000..c4b236eeae87ef90f3f982d047fef48285509bbb GIT binary patch literal 1879 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 literal 0 HcmV?d00001 diff --git a/Application/src/main/res/drawable-xxhdpi/ic_action_device_access_bluetooth_searching.png b/Application/src/main/res/drawable-xxhdpi/ic_action_device_access_bluetooth_searching.png new file mode 100755 index 0000000000000000000000000000000000000000..de264301cad9264a67a8c78f0c9e849bbed494a1 GIT binary patch literal 3083 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 Ah5!Hn literal 0 HcmV?d00001 diff --git a/Application/src/main/res/drawable-xxhdpi/ic_launcher.png b/Application/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6beccf3922d1b6e77be59c6a4aec96ebf9fc7990 GIT binary patch literal 12071 zcmV+?FWAtDP);E57u%HA% zgMx?(qBQBf_y76L%$c2?oh|phcb^Zr>t}u3-Pzf_Gv7UJ&de<*3R|HCiqQh)ig9h> z(n1S}0zd@_iq!%IfQr?8g^LRS6&NU13lsneYIcFJHc8IZ>|Cuec-A05Lq3Cw@BVs)qF&+$rUdmp?6+>~l$F z-TvXS_EVrP{>=Z*QorxMc0IVa@3Ze7T$b-^*X`Dk#GLy~zV8*+UcPJN`oMnPr1~Ts z)&tw^@2i{6FIl>DK!?NHKSEb+@s?2+9K<76w3X2TVTV64fE>Ot=o>i;X2wk(+=B6 zZZ1HOKGkTivv~31x!bmxa=+fzDSWfk79jgqG-}kSB^6vr`+8dnxd1`>)S$h={Q2`| zq}E(0u+y|avu4fqpf9_W_O)6Hc_mO~I$E3dhV$mlo3_)mPT`BDwm^#(E%u^9OK4w3 zI}B zFJC^jef#!jSrRA~AgC~D1JT};_WHEfq8-O&eCYgJnm&ntmMh0WYWYc&FAw{h{!PcX zlWVAsaR6J>TCTh(PdnT0Tvi>!wdyan^5vZRs9u~$y`lTa=a5JA|M}U1`s`=r`nevi z%iK6IMJfoEP_WHhvON+cqa*tK_U+0M!RMfyx)#Uq_@*pMP~Jb(-U;x{&BE7lK4~w!Y2)raKUyxwBPW z0c;GHD5s904pWw{13jgOS8VxtDd!qlmGw38-bwGp&G+cLh zUw{bq54;Kh z818H-X*77M9)7F55 zzg;=6BOPZ~9iRXve1`Y6fp$or*f@|>QyLfs2k_vybn*NCt@U>-8Mt{WNcDv$R=}Xc zq;xSVDNtwVd|bz>O#amXsV-6A1k$WRxZ@+LF_46oRtcnwCFMdAk6TamKxi{k(K;H5 ziJ4a~xR@mZF5^zLrH%Y{T4gC|!PlxzXRzcW0T5O-M*V4s3_z+5sm|oTl*Cfc8h|V% z5+Kq*;oMdUlImD<9hDi}YtHZJ5WDKA1Q0Pukufn}@STktyteizJOHYB)swA%4&Ksm z=PAjc)RQWsVUP`{sUm7S@eYQ$7>dR7{ zI5x27?B;ZnHMB$WunxtSvq$Dl^Z5X>+t&U!R9mh7!&)yiF^nzy%z&s3clE_xkbkAma)AqSBh|Fhde5_J4`Dp)xxHi zZNRdGlK0SdiZO(#Q5XsvBI9vcwhpT^R#nQjQN66BP#~?iPL^IMi+kRG(NFmW=!O?} zU*bxMkTlQ|!nKQ((X0(xWAg-`U6{DM1XV(%yfFrl;gOIUmp?F;14CJXVSslGYJP{< zs7Ki|4|zaLo%?SwZSH9A>*aIDQPA!nb)ixxE?79j2Cz>(Np+-cB4wel(X8Ec+56gq za?W#k*WSS;w(HLThSV(G^WF=7%r8JU{eu8AVy;LD;}Hw2gfQ zNJAc-hKKDh1ECQMjyI;tpVM)ms9w2_nDo=@V)9RKLSa}S1^*7DlH(z1aBPSe0ybDp zT*xwzZB@r=%D#?m80B^c(uCdcxbsRqx!#`t^WEpoEPa5I0dRo{KC>rg8{f7UW}kP# zV725cBk>4H8apJQbj|B|d zNp1%-5Ui5y&kULEIr#xnc2^bv?faJkKzl54-~Kp&k^pg~N$9g1>tU~prI1I`cqJ06 zJWGWsY036Z7#sy&IdRsZ4@-b}n>_n%@!y%Rni!7{B#!2~5$DKpSK?^(6nIfu6&epJ z>R}l$SgJ56p3iomFpC5$Re;>LKQk~EAW@Ia@9KwwhZ-OgAe7MtTk6<>LtFZU;j!%= z8>+JoeJBJFAo%j9k>aaaZz?Hdxos>7g5!Z|V6~ynv1i1RO#QVx*j6*Hn(|92zYCuP zn0r4LuwM+II0@nclKeT%K8nVYY?T*+EXM8$*-KjDC`l7|VJ<)n$K=^>5lE!YLJ{L2 z5ND4m1(Jy4G8R}KDU=3Cnw{gfi_!sefx7E|e#o~C6sWLRi4!Yf{Ps`aQR%>=cF&`5 zL;>@aVO7NEf*@v~y6hO0@x-)hJM)kSLlTHp8Klj3^TvpYGhY^4x2iCS4N4X%lx9r3 zc8kvrN|PeNJ<2LTk?|;Zd@A@{;`Wha|M*p)mQ;?t!~ry8!6%|s!vjR+ipaAIZ%bDHD8_vKs93WB6BjsE6paZ* zB$1`UvbpN%cFazy!qOyAybEtjAfKw@AYpL<`e5o1v1Q8!al~GiiK>;6^&8%nuKr1k z{(6vvC>oj8FuAI%0FYlw6emVxM$XHa1Oh;FmV7Ci*WX(l-S*F-Vg+RWMkUevlOHB^ zwmf7Y+wQrNPt;J3)l0bWuqJb+&5# zLdV1M7}Gg;VQDHZWMuDrHc%9h+}-&6dSL))_R=pkEd)_Rf~XB3ib|ptYv+mgr#vE- z(_fZ2A<0vO;f#V8rqa5?#%@AXJ9mH>!ni)-u-d3(#Am01x0kM4Dyr@l70szHHoL_|z{cDUw+W^_gWV1);m$D@ z<{970md;T+qw&9M(McTHrl(l7VWAi^Wsq35X0`>8hAAPvHEuK>0L@;)$@b9<#OAb= zK&O5Bh$NFjqUMO^aj1 z;d@;wDwM-~NSYx>(P5~bMHDTLmqH3E-hBB$I5&o3JhRD-w{0M47+cwcP-n*wL_Y6q z}U+dpE^;_d%r z+TX>jCI1bns;n7_4Ay9pW^-t|Ph3V~d4l%9-Om<#xBP>XtgmWVv}y(c^srdHj@)}a zcWbB4NK6a(71_=yVor3e>W#&5`(7;?)!sd#Lbh((EGEo&iKgnjCueT3UBn8FFYx6= z2LKH;D$Gr;uUe^=G^AV6oHF0`R$J3q=>o=~9NFO`CBPCkeI9Unj3fcEBg_PFl zw4SmwD+8nGo!a&iyEJSc=|^K>Ol_{{-WxuNh>=Ac^ zd{N0WI=^-M7BO-9 zKLir-&FoVtx2)M1DbmNAXqKyb%OR7oQ~CtcFFhiKNRD?e}U3cvFuIYg`IoYqp<)%g<-z`0lS?g+O;~- zOklKcuzAaRG4|W1#PsmN<~n}CbW+qs7g_5uVKd@*@afO6MH9lTH?@{BRF1uJM7 zB`I-og(tT(*EIE&?R)@isR87TK&Op!<$B&1b(VHZbdEeaE0n_OT-&zg@#2u*p6`v% zELicaOa@!GafO5|4xle*y&(rs>(=lu;J_Rly7pj`!@=_+W9=-hZY@F;?#jZ^$Tw z73b(VF`d~xTJ*?`{jr+hE(^3rZ{ zZoDtWG%W=+EokU9wle_}hQ%J&Vq1-oYFWRX^qQ*C>KpF1cEeJEq_Ix>^d^9?P;Itt z+PwG0f4+ZS!1gimAq$eCz?0;im`D4x);L1QMv7WjEMG-b@T6zBg|}|oBInG1G4qv>bTJahO8fBHLJF*0 z=E`R?o{QwYXGsRYJD2`{EKKoYAu&-GbPrKn*4wnS?6or*{M-= zrt!+r7V;02hJ%lZxkH(!2yGXo3{Y->q%9Z~GM+O6V*{)tQCKn>Y+}a0s#K~jI_=#@ z?ArLC%nZSk8u#7P;_KP}C$DkG#X@TYIR>*SpV5%^#fq{T*O|RH3$T0tG&{emb6o!- zto}^mY}+J8QMxLo zHya6Lpa4}mU&u=JNQ6gT;;bQ*Fmt0QaJXKePeqm*K)xmCTnUo2reWqZ4ey$VfxVLk z5rY$qz=uJCOAj@civ(g|*3r}*WEw75{;eg26g;+6;`EQ$prQp&bWk<7g{X?|y(yJg z3IMfIi)^|>!hmUK^2S;=*e|pCatk71?ahfd zi)Cx)M!=C0$Fg^f1hS^`$oev>v*OIJ$8EB#0&?H^bMmdi0)R?H3PF(+Vf9rvzT>X_ z!_TI8ist{+uHHnP)P8`dUn`n|kto%>R<$vpX`9dW7?ayx&t9fp`HcV}BLJ~;|9{$Y||GGh0BiC88 z8ndXkQU@rD6iRaYV^cLUdSQ%9me>!_gu42}e%Fd>RiYrQS-+Uv^9LndHLK!93~v}O z+mmK{w5-3c7ovH~zZUO&al6=nfmz3h4#DHGk%~L$niTg*j$DTaZ}Se&wZmI#FHDcf zr>LM{L4#Zt36Bd3`lXW%k&!=^+T!u6-!wUta;E!vr`}+c{ax~^Fr)P1qo4Hx1U2>Z z@^8cm``ECO0&H;p9;MHf_eE> zVKzY761|dDQu2x%<;1?NPL{JZDtYFzLvp-3={~)_j}YBDJ?91JZIUj? zZHC)_@&UKdkjS21NrdS1dnCyatxmFo>~oP^Axp&-ZHZtj>TK#tR+YHV0~h?9XMkR4 zVMU_Dwh-GSZd@n|nr|Vf1nH7P?8lWxNd7u(&u-#?-<;)5{`zV0L>bS4?Zd8q_fF5p z0Cby}xp2IMq81t9ryh7a4TZGwLWJaf%)mfqV#&JozI>r<5-Q8uM0p43nirbOu`q0j zk>wn6dD7f=znv3x+ZDhHYHToskER6seOq_+cJeoK-lb%+$Bm?53D*6X7h(bWfm9T4 z7)R@R;BBIAb^OFubi*JkW@6ZD1tm$Ouqx9fa~bx?qPm;}H}~fj9=KqBekG6uD5HT} z6wu<-q8}nYO29a33pvr9+w~KeXy|TeG>oE;rMY5kr`Tn>A2VFks@f=W-CLjkRm@zZ zB#`W0>Rdqd$Aj({=>@}qG*O9*HdhGQozXS(Uk%~%}gLMNESo4TWZM} zk4lp;X+SE;l6|zja)oN*ln%Fh(@xP*<0*INv+2WwgnBC<8uz;7*cZJ3z4`eqMui2D zW+9r_YfI|v4)3aEn2!b(Id$%+%$`}0;!(VG*reDIzNgRqfAGQu`IbOofQk!8(oZ!N zWuUZ1)du1Z`(G!1)A$gtIbVY!}H)novfki+4&Vw zt6J0xg9@6l;L`{g3{&ey2Z&P+xSfpY=rm`jFc{%KEdo4_9h7OayZjr|xoljw1UrIB@w15zNBELiM#k7Wb7VEy^H*Sr9|MgUD;FqVtU zzi{7A>u{&neP6a_ju|WP^}2Sjtz*p0aYFgX>iZdvlFvzP8tKBfS8P`0QFSWBwH{=JQ z{Lv8r0ySpmUn=zdbUA04kY_4qdlZM*C(|MIx%)m=qH-A z#}&SYyU+sGx?c#Kko1Yhf_feQhIi882(n`k3c$8ki(|zJzrV@rZ7rmQ6-G|HQ7l`H z#XU7YI=S9_b`tR29BNi&2C$52u-@{%nIV?lW9MNHym(oDCD4`6){k}LRezG*GFHjn%-Y2~6&6mdS#lAB!sZcZ62LK%#xs}9NT?!5uA{JVT*w6%#|L#=kzauNK2z7@#^(xAw7e29>9b1X z4QY?0iHSD*TzA{R?sLLBUVuX4%KCuC$45@MUaY2^Y6AnG_ZZI+z%qMg@q?{C7}O*9 zvk>FTmkL1U?Tw2RXOcw85ZU)IB|Ul0b{>!oA@`pz4iqPS7_2@g{BLG}@axSZ{&Tfh zPs0>ui$D`}q~R*Vtc^4qDM^wP1s=dQsN2eXOQ1S34pib0sor;{9|x|!-RW_`#JDoH zFXY>*Nnu1p*t8pqJJX#vQruszy+ zRek~b%hRFKHd>ufm_&F!&aafdowXza3mPkJ{Mpl>Ka?9GX{ z(CiD9IL~jq4MWh;0W@ysbNM7&$6S-iM#`w>G|2Wi84i^SK(h0*+AEnMGDxYuNEvnK z(!sN+BldBwCvwa@zl~7<;FTwhN(5-~?00DX*KK-r*T7&7Fw!2FC-E#@;%Hp54tvCa z5SB>Ulh_7#U!7kG)aU71%7`u|M5a?SjJU@_5*u+&hKW1c8;PcqSDpMG0o2r{Oa2?3 z^7)K^icwP@q95PgXpV&nCP1V5+E!okKyth&N4K7z({|PQzELR`l~#i0VZ<5<@*jNjn1a9pB_eq=TIRUVs4fmTA*th+SYDea zMOxu>=>Vk2F@d+t>dd+>X(3GCVhCE~IrsEPnpBtq`D05CTN+(;@@TmPUI4&0x%lST zZ)l2+mPBFk6a)>^pfM7{f-HWNUX@OgAfa=`2nXmsO-L-}Kcl}TT1QPP%Qdb(pyWY9G z?Lpn@-abj=hbbt}9Tzd49UJKCt{+iObVFJ=e2Dm%W^zOo6xHO|Kyfi3*=;_kvP7uW z#hC|O`Cdf{3GlU#^;n-@33SC%)pQ&vpAb2aq~!Nx8F7yr4-1JE{h=S(K%degnx9R7 z(RVuLSak3-Bxy7Zkz=WW#IRH{k|?vilTGsQVD8W!8}bWKucxY7=^RCa$SbMT!DJ)Z zF_3*w#HlQw%N}P3?bV_S{YV`BvSEhJo!VnM*pOm@Wn@_xwvhD7$vgAFQB0|ocYyx< zL{*topBU&%fFXV`G+8Yrd1j0;_gRvGGn8n3000i+Nkl-TOn?$Ib`({gA6XMBUc?PJ*qg9Lq3cApa zOe=&8NS6Ilo_@PaZMu_mf8aO0!rq!whx`~npUXGSmF9iLMWNNLq7eC1mFXiNSUGrO zk$N&1*xR@1^%c#ds>a+#$K#A>8**1|GK|CivZ=%+5C>T*(@kn${7|KZih+20MM?sz z!_%aS4PsnbKxUw146XDI7iN301U_f~O#;YQKIvGjOK-?lz%lGw$$Lx zPmeSC<41wyi&t^D$Fu!6W989Rv~SW6nJ9LXdj-8$0YDerSLr9ySylkgND!-| z<3_-3_nx6}rIT$dxt$k|4XGL;0O6>ScxpCU+?H%J`LD_FKTer6WXO=-Lx&Fik*)d6H zzFb2DyC@`j;{d`kTlh^#Smcc_zx?tJci(;YJtgp5g(`NC7P#@o8*h00@yD^0$RgU| zd9S1$i(jV#1og!LVTFOl6)IGyH*DCj%Xi;>_YPI6REhp#?GDl?g~v>9fsGqCuKnSM zA0}RM$t90(-MST$2LNGbfI_3nNfHRQPkpE`+OcK{R!o9nT^2SxH&kt<;k9lv(%+I9Ebb5H*{bLRZ48zZ8>orh(~TZ5$St2JuWFd(8%dNakPmtJ}%T?2y> z4H?ew2v^2-WVKy(*`+zDgNN$WsZ*UKxp_u_v~uOjB{$u4Q_nSP)}W&pqS9)^xcdNN zzzm~#od$<~{6hj2dB!9!3~vxr8Nh*2jRxjW@-O)%`R%TD#~pX{Zr{HBC8Sp6n+<@l zZQe`^YJT+53j+rZ{0Du67)ArI7ZLaD_>Oo{>b~~cYoiIy5I`F?Y!GLjdFHWn9zP6^ zh6Rvu8+);M0qgbc+xJ|Ok{3d%*-{A4J7vn0VK?7=^AO!ZX;QMZ+8qGl1;KYjyWxew zv*B2(7lwyq6~RG7h9{i*S6W{ud>{FCckSABk3N0+yjQz+?W%|+LB+7jS+r=;{EIHS z=!*60*JF}83_u1bNksr4NZW?*zyJRDTD5A)`dF#~AX1lDI~esroF@TS%Gq5l0}1$_AyHmf_#md2gL@#u?r9bCv}uWJfs}DDOA@rGdy5IH?*Qc;JCf)R}KlXOx4KKxeQ4O@{NA z=bwN64O9-R9K?DLo*Ns_lK`3=0}y`W$*wc$`CV_e>G0vh@7Zgwz4o_NuC}kXj~h4c zlxwfOb_`XRrfPF-v%HqykpRN8RwmU_^VL^h{h&#cChaO#tQe9$>(;H?`q#hywJ&)$ zleF}K_mD;Uppi=&AX^H-n{9IBkw><_>#n;7kwIMnMn6j-NYAB9mrgnR?6W(qUcDOX zmIpHP+W93>`HlpTH0sGi>aov0`#e&;dUc}$SPFgi*=PTI<&{?sq;hQgz`M!%Mwc`| z=pQzw5e7j}wiy}rR~~lQVJEUd&Gryrnl^3PRix^kqH=7smP8-twT>MHAZa+Cc;bmo zuDkBKiFNDNZDhZ}Py^%=ucnwwPjcI78F*>?!25!?l+~ul2S9AI!Mnq-P!n?NcOx6^ zNs2twf&{erlUC!JtlsZ$|*gxa}{${PH~%4ewR8YE{|r*jf6} zxXqLI-h1yq=^LZ*ZBHpF;Ab~bJ^(`f921fr9_>x9zyA7jPe1+i%OFK1HZ^*03icsr2qPD< z8t#CsZI>^AaDS*@MDp=VzZe>7He$qx$HTvyz|aG zit?*&r+_7S^?_RtgL>h@g`+3~;v6au6`U4+C{ZACt*opABwgV_g9e>+$RUTk%p#xN zcBn8iJSTMP*6kJz57wCT4*F$in0x|++p}8_gBpX>P01FzyTbplyj5MWZlx}^84juM+@WBU%kW)|@-_3UGSFBj^{RJ0Xa0E#syb!rc z)BF+R)LfPUl6J#$&pp?tZQHhY!R^IciblqUzhlRa9Z3KUrH`)LUKG0_-vHsUr9DSG zyY)@U*16*7qmMq_4ok2UnlWR>fD13Y@KGwCt39{V10+2cvJ-ac*|X;adPD2FZu^oY zOP14k*JXU3a&6+i<=W**-Jn_U62b1@S z?^q_?`m!j2Ozh;9S6+E!x7~K@=5yOA8a?>ohaY~88kXI5j^GwW`rwT%4S-k*!Jw7{ zt+Y2KTj!7GoO4beNFkQ4kV25MbLY-|=B%^Mx=O3&GUwKpMSx`V_SRc(J+MocE~Ck9 zujr0<&7M7b1~qD54G+8R#f{#U7C>NxQ)S@R z&bQoh3(`xoM%8y(6=ued1`i(m#-WEEdW7q?Q(DD#8VuP!dS{FI+e<09oO4w0g|pR zc_(L4$p4xB_upR}eDJ}7le=^R)>z5|x>Li$ZhJ|*nFgTIqen*oLOxLE&YepNAapQ~ zlOcr!Qm84-$GVBW^O5YHpsbnMs>F8q2*0qr$)>eO$kEE~>vD@(#QFaRBY{PC0N{xV!Z?z9P9 zju|tiBmpwHP4xV7oNVvC_ijTex4*D{gc z%;HwM?G!M)mSR5xka$InB>{pB1Ot=op?^}u`R`i#@OW2Ah5HRaWP44f!A=Pfz4uFSagWkmv+0mn|A zI(g9Z&GxB!TUPAKkF z(g30Tp~_?~HtiTE+lAEj<0JtaLzO`S;YHGs>J^miwkMV2l-iPORsdon8pb5R!H&*@YD0h!4TdB!Zn%;pPpL-7!KejT zJWm^qhK|cu48)$&4SNhb-hOP5f>4{-y;FRBL=@o|KLfry+JM(104VtGXal}~NqL_| zc+B9EvLb=-L<|wHjE28xP&6K(;Z?Ho<$2)#+<|!Oc{(?qji>oS8s>W;1gJ7@1H4e& z26&O|HYgJdF_a~M7$WXSdmcWo(U&+&vRJQ@y4b!quMvske1H2nO5k?hD)0=yWE + + + + + + + + + + + + + + + + + + + + + + diff --git a/Application/src/main/res/layout/activity_device_list.xml b/Application/src/main/res/layout/activity_device_list.xml new file mode 100644 index 0000000..ae7242c --- /dev/null +++ b/Application/src/main/res/layout/activity_device_list.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + +