From 8247c1932329e11d768776013fd28e44768a060d Mon Sep 17 00:00:00 2001 From: Starcommander Date: Thu, 26 Oct 2023 11:00:15 +0200 Subject: [PATCH] Add bluetooth service to transmit or receive settings and maps. --- PocketMaps/app/src/main/AndroidManifest.xml | 5 + .../pocketmaps/activities/ExportActivity.java | 284 +++++++-- .../pocketmaps/activities/Permission.java | 31 +- .../bluetooth/BluetoothService.java | 574 ++++++++++++++++++ .../pocketmaps/bluetooth/BluetoothUtil.java | 307 ++++++++++ .../pocketmaps/bluetooth/Constants.java | 35 ++ .../downloader/MapDownloadUnzip.java | 1 - .../model/listeners/ObjectRunnable.java | 6 + .../src/main/res/layout/activity_export.xml | 28 +- .../app/src/main/res/values-de/strings.xml | 6 + .../app/src/main/res/values-es/strings.xml | 6 + .../app/src/main/res/values-it/strings.xml | 6 + .../app/src/main/res/values-nl/strings.xml | 6 + .../src/main/res/values-pt-rBR/strings.xml | 6 + .../app/src/main/res/values/strings.xml | 6 + 15 files changed, 1263 insertions(+), 44 deletions(-) create mode 100644 PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/BluetoothService.java create mode 100644 PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/BluetoothUtil.java create mode 100644 PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/Constants.java create mode 100644 PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/model/listeners/ObjectRunnable.java diff --git a/PocketMaps/app/src/main/AndroidManifest.xml b/PocketMaps/app/src/main/AndroidManifest.xml index 044386a..32cac8f 100644 --- a/PocketMaps/app/src/main/AndroidManifest.xml +++ b/PocketMaps/app/src/main/AndroidManifest.xml @@ -14,6 +14,11 @@ + + + + + diff --git a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/activities/ExportActivity.java b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/activities/ExportActivity.java index f33454f..b0c2a3e 100644 --- a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/activities/ExportActivity.java +++ b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/activities/ExportActivity.java @@ -3,10 +3,13 @@ import com.junjunguo.pocketmaps.R; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; +import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; @@ -18,17 +21,29 @@ import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.Toast; +import androidx.fragment.app.FragmentActivity; +import com.junjunguo.pocketmaps.bluetooth.BluetoothService; +import com.junjunguo.pocketmaps.bluetooth.BluetoothUtil; import com.junjunguo.pocketmaps.downloader.MapUnzip; import com.junjunguo.pocketmaps.downloader.ProgressPublisher; import com.junjunguo.pocketmaps.util.IO; import com.junjunguo.pocketmaps.util.Variable; +import com.junjunguo.pocketmaps.bluetooth.Constants; +//import com.junjunguo.pocketmaps.util.BluetoothUtil; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.GregorianCalendar; +import java.util.Map; +import java.util.UUID; public class ExportActivity extends AppCompatActivity implements OnClickListener, OnItemSelectedListener, OnCheckedChangeListener { public enum FileType { Tracking, Favourites, Setting, Map, Unknown } + public enum EType { Export, Import, Transmit, Receive } + BluetoothUtil btUtil; Spinner exSpinner; Spinner exTypeSpinner; CheckBox exSetCb; @@ -36,15 +51,24 @@ public enum FileType { Tracking, Favourites, Setting, Map, Unknown } CheckBox exTrackCb; CheckBox exMapsCb; TextView exFullPathTv; - LinearLayout lImport; + TextView exStatus; + LinearLayout lFList; LinearLayout lExport; + LinearLayout lReceive; LinearLayout lMaps; + /** Returns the selected Type. */ + private EType getSelectedType() + { + return EType.values()[(int)exTypeSpinner.getSelectedItemId()]; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_export); Button exOk = (Button) findViewById(R.id.exOk); + Button rcOk = (Button) findViewById(R.id.rcOk); exSpinner = (Spinner) findViewById(R.id.exSpinner); exTypeSpinner = (Spinner) findViewById(R.id.exTypeSpinner); exFullPathTv = (TextView) findViewById(R.id.exFullPathTv); @@ -52,9 +76,11 @@ public enum FileType { Tracking, Favourites, Setting, Map, Unknown } exFavCb = (CheckBox) findViewById(R.id.exFav_cb); exTrackCb = (CheckBox) findViewById(R.id.exTrack_cb); exMapsCb = (CheckBox) findViewById(R.id.exMaps_cb); - lImport = (LinearLayout) findViewById(R.id.exLayout_import); + lFList = (LinearLayout) findViewById(R.id.exLayout_list); lExport = (LinearLayout) findViewById(R.id.exLayout_export); + lReceive = (LinearLayout) findViewById(R.id.exLayout_receive); lMaps = (LinearLayout) findViewById(R.id.exLayout_maps); + exStatus = (TextView) findViewById(R.id.exStatus); exSetCb.setChecked(true); exFavCb.setChecked(true); exTrackCb.setChecked(true); @@ -62,22 +88,174 @@ public enum FileType { Tracking, Favourites, Setting, Map, Unknown } fillTypeSpinner(); fillMapList(); exOk.setOnClickListener(this); + rcOk.setOnClickListener(this); + UUID uuid = UUID.fromString("155155a2-e241-454a-a689-c6559116f28a"); + btUtil = new BluetoothUtil(uuid, NFC_SERVICE, this); } /** Import-Filebutton or Export-Button pressed. */ @Override public void onClick(View v) { - if (v.getId()!=R.id.exOk) + log("Selected: " + getSelectedType()); + if (getSelectedType() == EType.Import) { // Import a pmz file. String dataDir = ((PathElement)exSpinner.getSelectedItem()).getPath(); String dataFile = ((Button)v).getText().toString(); log("Import from: " + dataDir + "/" + dataFile); new MapUnzip().unzipImport(new File(dataDir, dataFile).getPath(), this.getApplicationContext()); finish(); - return; } - log("Selected: Export"); + else if (getSelectedType() == EType.Transmit) + { + String dataDir = ((PathElement)exSpinner.getSelectedItem()).getPath(); + String dataFile = ((Button)v).getText().toString(); + log("Transmit from: " + dataDir + "/" + dataFile); + if (!btUtil.isSupported()) + { + logUser("Bluetooth is not supported"); + } + else if (!BluetoothUtil.isPermissionAllowed(this)) + { + logUser("Bluetooth: No permission"); + exStatus.setText("No permission, try again"); + exStatus.setTextColor(0xFFFF0000); // 0xAARRGGBB + BluetoothUtil.requestPermission(this); + } + else if (!btUtil.isEnabled()) + { + logUser("Bluetooth: Not enabled"); + exStatus.setText("Not enabled, try again"); + exStatus.setTextColor(0xFFFF0000); // 0xAARRGGBB + btUtil.requestEnable(this); + } + else if (!btUtil.isConnected()) + { + logUser("Bluetooth: Not connected"); + Handler h = new Handler() + { + @Override public void handleMessage(Message msg) + { + if (msg.what == BluetoothUtil.MSG_FAILED) + { + exStatus.setText("Connection failed, try again"); + exStatus.setTextColor(0xFFFF0000); // 0xAARRGGBB + } + else if (msg.what == BluetoothUtil.MSG_STARTED || msg.what == BluetoothUtil.MSG_PROGRESS) + { + exStatus.setText("Connecting in progress"); + exStatus.setTextColor(0xFF0000FF); // 0xAARRGGBB + } + else if (msg.what == BluetoothUtil.MSG_FINISH) + { + exStatus.setText("Connected, select again file to send"); + exStatus.setTextColor(0xFF0000FF); // 0xAARRGGBB + } + } + }; + btUtil.connect(this, h); + } + else + { + logUser("Bluetooth: transmitting..."); + exStatus.setText("Transmitting in progress"); + exStatus.setTextColor(0xFF0000FF); // 0xAARRGGBB + Handler han = new Handler() + { + @Override public void handleMessage(Message msg) + { + if (msg.what == BluetoothUtil.MSG_FAILED) + { + exStatus.setText("Transmission failed, try again"); + exStatus.setTextColor(0xFFFF0000); // 0xAARRGGBB + } + else if (msg.what == BluetoothUtil.MSG_STARTED) + { + exStatus.setText("Transmitting started"); + exStatus.setTextColor(0xFF0000FF); // 0xAARRGGBB + } + else if (msg.what == BluetoothUtil.MSG_FINISH) + { + exStatus.setText("Transmitting successful!"); + exStatus.setTextColor(0xFF00FF00); // 0xAARRGGBB + } + else if (msg.what == BluetoothUtil.MSG_PROGRESS) + { + int area = msg.arg1/10; // 0-10 + String bar = "["; + for (int i=0; i<10; i++) + { + if (i { // Because this may be a long running task, we dont use AsyncTask. exportMaps(zipFile); - }}); + }); t.start(); } finish(); @@ -169,18 +341,49 @@ public static FileType getFileType(String file) @Override public void onItemSelected(AdapterView parent, View view, int i, long l) { + exStatus.setText("-----"); + exStatus.setTextColor(0xFF000000); // 0xAARRGGBB + boolean reloadFiles = false; if (parent == exTypeSpinner) { - if (exTypeSpinner.getSelectedItemId()==0) - { + if (getSelectedType() == EType.Export) + { // Export lExport.setVisibility(View.VISIBLE); - lImport.setVisibility(View.GONE); + lFList.setVisibility(View.GONE); + lReceive.setVisibility(View.GONE); } - else - { + else if (getSelectedType() == EType.Import) + { // Import + lExport.setVisibility(View.GONE); + lFList.setVisibility(View.VISIBLE); + lReceive.setVisibility(View.GONE); + reloadFiles = true; + } + else if (getSelectedType() == EType.Transmit) + { // Transmit lExport.setVisibility(View.GONE); - lImport.setVisibility(View.VISIBLE); - lImport.removeAllViews(); + lFList.setVisibility(View.VISIBLE); + lReceive.setVisibility(View.GONE); + reloadFiles = true; + } + else // if (getSelectedType() == EType.Receive) + { // Receive + lExport.setVisibility(View.GONE); + lFList.setVisibility(View.GONE); + lReceive.setVisibility(View.VISIBLE); + } + } + else // parent == exSpinner + { + String dataDir = ((PathElement)exSpinner.getSelectedItem()).getPath(); + String dataDirShort = ((PathElement)exSpinner.getSelectedItem()).toString(); + if (dataDirShort.endsWith(dataDir)) { exFullPathTv.setText(""); } + else { exFullPathTv.setText(dataDir); } + reloadFiles = (getSelectedType()==EType.Import) || (getSelectedType() == EType.Transmit); + } + if (reloadFiles) + { + lFList.removeAllViews(); String dataDir = ((PathElement)exSpinner.getSelectedItem()).getPath(); for (String f : new File(dataDir).list()) { @@ -189,17 +392,30 @@ public void onItemSelected(AdapterView parent, View view, int i, long l) Button button = new Button(this); button.setText(f); button.setOnClickListener(this); - lImport.addView(button); + LinearLayout ll = new LinearLayout(this); + ll.setOrientation(LinearLayout.HORIZONTAL); + ll.addView(button); + Button delBut = new Button(this); + delBut.setText("DEL"); + delBut.setOnClickListener(createDelListener(f)); + ll.addView(delBut); + lFList.addView(ll); } - } } - else // parent == exSpinner + } + + private OnClickListener createDelListener(String f) + { + return new OnClickListener() { - String dataDir = ((PathElement)exSpinner.getSelectedItem()).getPath(); - String dataDirShort = ((PathElement)exSpinner.getSelectedItem()).toString(); - if (dataDirShort.endsWith(dataDir)) { exFullPathTv.setText(""); } - else { exFullPathTv.setText(dataDir); } - } + @Override + public void onClick(View v) + { + String dataDir = ((PathElement)exSpinner.getSelectedItem()).getPath(); + new File(dataDir, f).delete(); + ((ViewGroup)v.getParent().getParent()).removeView((ViewGroup)v.getParent()); + } + }; } @Override @@ -234,7 +450,7 @@ private void exportMaps(File zipBaseFile) mapFiles.add(new File(mDir, mFile).getPath()); mapSubDirs.add("/maps/" + dName); } - if (mapFiles.size()==0) { continue; } + if (mapFiles.isEmpty()) { continue; } ProgressPublisher pp = new ProgressPublisher(this.getApplicationContext()); new MapUnzip().compressFiles(mapFiles, mapSubDirs, zipFile, pp, this); } @@ -248,11 +464,7 @@ private void log(String str) private void logUser(String str) { Log.i(ExportActivity.class.getName(), str); - try - { - Toast.makeText(this.getBaseContext(), str, Toast.LENGTH_SHORT).show(); - } - catch (Exception e) { e.printStackTrace(); } + runOnUiThread(() -> Toast.makeText(ExportActivity.this, str, Toast.LENGTH_SHORT).show()); } private void fillSpinner() @@ -272,6 +484,8 @@ private void fillTypeSpinner() ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line); adapter.add(getResources().getString(R.string.exp)); adapter.add(getResources().getString(R.string.imp)); + adapter.add(getResources().getString(R.string.transmit)); + adapter.add(getResources().getString(R.string.receive)); exTypeSpinner.setAdapter(adapter); exTypeSpinner.setSelection(0); exTypeSpinner.setOnItemSelectedListener(this); diff --git a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/activities/Permission.java b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/activities/Permission.java index 41580d2..80ff061 100644 --- a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/activities/Permission.java +++ b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/activities/Permission.java @@ -15,6 +15,7 @@ import android.widget.Button; import android.widget.EditText; import android.widget.Toast; +import com.junjunguo.pocketmaps.downloader.ProgressPublisher; public class Permission extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback, OnClickListener @@ -34,7 +35,11 @@ public static void startRequest(String[] sPermission, boolean isFirstForcedPermi Intent intent = new Intent(activity, Permission.class); activity.startActivity(intent); -// activity.finish(); + + if (isFirstForcedPermission && !checkPermission(sPermission[0], activity)) + { // On new Android (13?) we need to ask for POST_NOTIFICATIONS, and first notification is not shown. + new ProgressPublisher(activity).updateTextFinal("Welcome to PocketMaps"); + } } @Override protected void onCreate(Bundle savedInstanceState) { @@ -89,7 +94,7 @@ public void onClick(View v) if (v.getId()==R.id.okTextButton) { log("Selected: Permission-Ok"); - requestPermissionLater(sPermission); + requestPermissionLater(this, sPermission); isAsking = true; } } @@ -123,11 +128,12 @@ public static boolean checkPermission(String sPermission, Context context) { } /** Check for permission to permit. - * @param sPermission The Permission of android.Manifest.permission.xyz **/ - private void requestPermissionLater(String[] sPermission) { + * @param activity The parent activity. + * @param sPermission The Permission of android.Manifest.permission.xyz **/ + public static void requestPermissionLater(Activity activity, String[] sPermission) { // if (ActivityCompat.shouldShowRequestPermissionRationale(this, // sPermission)) { - ActivityCompat.requestPermissions(this, + ActivityCompat.requestPermissions(activity, sPermission, getId()); // } else { @@ -136,7 +142,20 @@ private void requestPermissionLater(String[] sPermission) { // } } - private int getId() + /** Returns true, when all permissions are allowed. + * @param activity The parent activity. + * @param sPermission All permissions to request. + * @return True, when all permissions are allowed. */ + public static boolean getPermissionsAllowed(Activity activity, String[] sPermission) + { + for (String p : sPermission) + { + if (androidx.core.content.ContextCompat.checkSelfPermission(activity, p) == android.content.pm.PackageManager.PERMISSION_DENIED) { return false; } + } + return true; + } + + private static int getId() { idCounter ++; return idCounter; diff --git a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/BluetoothService.java b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/BluetoothService.java new file mode 100644 index 0000000..10566f0 --- /dev/null +++ b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/BluetoothService.java @@ -0,0 +1,574 @@ +/* + * 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 com.junjunguo.pocketmaps.bluetooth; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothServerSocket; +import android.bluetooth.BluetoothSocket; +import android.app.Activity; +import android.os.Handler; +import android.os.Message; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.util.Properties; +import java.util.UUID; +import java.util.logging.Logger; +import java.util.logging.Level; + +/** + * 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. + * Source: https://github.com/android/connectivity-samples + */ +public class BluetoothService { + private final static Logger log = Logger.getLogger(BluetoothService.class.getName()); + + private final BluetoothAdapter mAdapter; + Handler mReceiver; + private AcceptThread mSecureAcceptThread; + private ConnectThread mConnectThread; + private ConnectedThread mConnectedThread; + private int mState; + private int mNewState; + private UUID mUuid; + private String mName; + + // 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 activity The UI Activity Context + * @param sName The name for this service. + * @param uuid The unique uuid for this service. + */ + public BluetoothService(Activity activity, String sName, UUID uuid) { + mAdapter = BluetoothAdapter.getDefaultAdapter(); + mState = STATE_NONE; + mNewState = mState; + mUuid = uuid; + mName = sName; + } + + public boolean isSupported() + { + return mAdapter!=null; + } + public boolean isEnabled() + { + return mAdapter.isEnabled(); + } + + /** Get the actual paired devices. + * @return A list with deviceNames and deviceAddresses. */ + public Properties getPairedDevices() + { + Properties devs = new Properties(); + for (BluetoothDevice dev : mAdapter.getBondedDevices()) + { + devs.put(dev.getName(), dev.getAddress()); + } + return devs; + } + + /** + * Update UI title according to the current state of the chat connection + */ + private synchronized void updateUserInterfaceTitle() { + mState = getState(); + log.fine("updateUserInterfaceTitle() " + mNewState + " -> " + mState); + mNewState = mState; + } + + /** + * 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() + * @param receiver The Receiver, see BluetoothUtil.createFileReceiver(). + */ + public synchronized void startReceiver(Handler receiver) { + mReceiver = receiver; + log.fine("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; + } + + // Start the thread to listen on a BluetoothServerSocket + if (mSecureAcceptThread == null) { + mSecureAcceptThread = new AcceptThread(); + mSecureAcceptThread.start(); + } + + // Update UI title + updateUserInterfaceTitle(); + } + + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param deviceAddress The BluetoothDevice to connect + * @param msgHandler The handler that sends empty messages of BluetoothUtil.MSG_XXX. + */ + public void connect(String deviceAddress, Handler msgHandler) + { + connect(mAdapter.getRemoteDevice(deviceAddress), msgHandler); + } + + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param device The BluetoothDevice to connect + * @param msgHandler The handler that sends empty messages of BluetoothUtil.MSG_XXX. + */ + public synchronized void connect(BluetoothDevice device, Handler msgHandler) { + log.log(Level.FINE, "connect to: {0}", 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, msgHandler); + 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 + * @param socketType + */ + public synchronized void connected(BluetoothSocket socket, BluetoothDevice + device, final String socketType) { + log.log(Level.FINE, "connected, Socket Type: {0}", 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; + } + + // Cancel the accept thread because we only want to connect to one device + if (mSecureAcceptThread != null) { + mSecureAcceptThread.cancel(); + mSecureAcceptThread = null; + } + + // Start the thread to manage the connection and perform transmissions + mConnectedThread = new ConnectedThread(socket, socketType); + mConnectedThread.start(); + + updateUserInterfaceTitle(); + } + + /** + * Stop all threads + */ + public synchronized void stop() { + log.fine("stop"); + + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + if (mSecureAcceptThread != null) { + mSecureAcceptThread.cancel(); + mSecureAcceptThread = null; + } + mState = STATE_NONE; + updateUserInterfaceTitle(); + } + + /** + * Write to the ConnectedThread in an unsynchronized manner + * + * @param out The bytes to write + * @param len The length to write + * @see ConnectedThread#write(byte[], int) + * @return True on success. + */ + public boolean write(byte[] out, int len) { + // Create temporary object + ConnectedThread r; + // Synchronize a copy of the ConnectedThread + synchronized (this) { + if (mState != STATE_CONNECTED) return false; + r = mConnectedThread; + } + // Perform the write unsynchronized + return r.write(out, len); + } + + /** + * Write to the ConnectedThread in an unsynchronized manner + * + * @param str The Message to write + * @see ConnectedThread#write(byte[], int) + * @return True when ok. + */ + public boolean write(String str) { + // Create temporary object + ConnectedThread r; + // Synchronize a copy of the ConnectedThread + synchronized (this) { + if (mState != STATE_CONNECTED) return false; + r = mConnectedThread; + } + // Perform the write unsynchronized + return r.write(str); + } + + /** + * Indicate that the connection attempt failed and notify the UI Activity. + */ + private void connectionFailed() { + mState = STATE_NONE; + updateUserInterfaceTitle(); + log.warning("Connection failed."); + } + + /** + * Indicate that the connection was lost and notify the UI Activity. + */ + private void connectionLost() { + mState = STATE_NONE; + updateUserInterfaceTitle(); + log.warning("Connection lost."); + } + + /** + * This thread runs while listening for incoming connections. It behaves + * like a server-side client. It runs until a connection is accepted + * (or until cancelled). + */ + private class AcceptThread extends Thread { + // The local server socket + private final BluetoothServerSocket mmServerSocket; + private String mSocketType = "Secure"; + + public AcceptThread() { + BluetoothServerSocket tmp = null; + + // Create a new listening server socket + try { + tmp = mAdapter.listenUsingRfcommWithServiceRecord(mName, mUuid); + } catch (IOException e) { + log.log(Level.SEVERE, "Socket Type: " + mSocketType + "listen() failed", e); + } + mmServerSocket = tmp; + mState = STATE_LISTEN; + } + + public void run() { + log.fine("Socket Type: " + mSocketType + + "BEGIN mAcceptThread" + this); + setName("AcceptThread" + mSocketType); + + BluetoothSocket socket; + + // Listen to the server socket if we're not connected + while (mState != STATE_CONNECTED) { + try { + // This is a blocking call and will only return on a + // successful connection or an exception + socket = mmServerSocket.accept(); + } catch (IOException e) { + log.log(Level.SEVERE, "Socket Type: " + mSocketType + "accept() failed", e); + break; + } + + // If a connection was accepted + if (socket != null) { + synchronized (BluetoothService.this) { + switch (mState) { + case STATE_LISTEN: + case STATE_CONNECTING: + // Situation normal. Start the connected thread. + connected(socket, socket.getRemoteDevice(), + mSocketType); + break; + case STATE_NONE: + case STATE_CONNECTED: + // Either not ready or already connected. Terminate new socket. + try { + socket.close(); + } catch (IOException e) { + log.log(Level.SEVERE, "Could not close unwanted socket", e); + } + break; + } + } + } + } + log.info("END mAcceptThread, socket Type: " + mSocketType); + + } + + public void cancel() { + log.fine("Socket Type" + mSocketType + "cancel " + this); + try { + mmServerSocket.close(); + } catch (IOException e) { + log.log(Level.SEVERE, "Socket Type" + mSocketType + "close() of server failed", e); + } + } + } + + + /** + * 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 = "Secure"; + private Handler msgHandler; + + public ConnectThread(BluetoothDevice device, Handler msgHandler) { + this.msgHandler = msgHandler; + mmDevice = device; + BluetoothSocket tmp = null; + + // Get a BluetoothSocket for a connection with the + // given BluetoothDevice + try { + tmp = device.createRfcommSocketToServiceRecord(mUuid); + } catch (IOException e) { + log.log(Level.SEVERE, "Socket Type: " + mSocketType + "create() failed", e); + msgHandler.sendEmptyMessage(BluetoothUtil.MSG_FAILED); + mmSocket = null; + return; + } + mmSocket = tmp; + mState = STATE_CONNECTING; + } + + public void run() { + if (mmSocket == null) { return; } + log.log(Level.INFO, "BEGIN mConnectThread SocketType: {0}", mSocketType); + setName("ConnectThread" + mSocketType); + + // Always cancel discovery because it will slow down a connection + mAdapter.cancelDiscovery(); + msgHandler.sendEmptyMessage(BluetoothUtil.MSG_STARTED); + + // 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.log(Level.SEVERE, "unable to close() " + mSocketType + " socket during connection failure", e2); + } + connectionFailed(); + msgHandler.sendEmptyMessage(BluetoothUtil.MSG_FAILED); + return; + } + + // Reset the ConnectThread because we're done + synchronized (BluetoothService.this) { + mConnectThread = null; + } + msgHandler.sendEmptyMessage(BluetoothUtil.MSG_FINISH); + + // Start the connected thread + connected(mmSocket, mmDevice, mSocketType); + } + + public void cancel() { + try { + mmSocket.close(); + } catch (IOException e) { + log.log(Level.SEVERE, "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 InputStream mmInStream; + private OutputStream mmOutStream; + + public ConnectedThread(BluetoothSocket socket, String socketType) { + log.log(Level.FINE, "create ConnectedThread: {0}", 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.log(Level.SEVERE, "temp sockets not created", e); + } + + mmInStream = tmpIn; + mmOutStream = tmpOut; + mState = STATE_CONNECTED; + } + + @Override + public void run() { + log.info("BEGIN mConnectedThread"); + byte[] buffer = new byte[1024*4]; + long dataLength = -1; + long dataWritten = 0; + try + { + // Read header when connected + if (mState == STATE_CONNECTED && mmSocket.isConnected()) + { + if (! (mmInStream instanceof ObjectInputStream)) { mmInStream = new ObjectInputStream(mmInStream); } + ObjectInputStream ois = (ObjectInputStream)mmInStream; + String info = ois.readUTF(); + dataLength = BluetoothUtil.headerGetDataLength(info); + Message msg = new Message(); + msg.what = BluetoothUtil.MSG_STARTED; + msg.obj = info; + mReceiver.sendMessage(msg); + } + // Keep listening to the InputStream while connected + while (mState == STATE_CONNECTED && mmSocket.isConnected()) + { + // Read from the InputStream + int len = mmInStream.read(buffer); + if (len < 0) { break; } + float percent = ((float)dataWritten/dataLength) * 100.0f; + Message msg = new Message(); + msg.what = BluetoothUtil.MSG_PROGRESS; + msg.obj = buffer; + msg.arg1 = (int)percent; + msg.arg2 = len; + mReceiver.sendMessage(msg); + dataWritten += len; + if (dataWritten >= dataLength) { break; } + buffer = new byte[1024*4]; // Dont override sent data-array. + } + mReceiver.sendEmptyMessage(BluetoothUtil.MSG_FINISH); + } catch (IOException e) { + log.log(Level.SEVERE, "disconnected", e); + mReceiver.sendEmptyMessage(BluetoothUtil.MSG_FAILED); + connectionLost(); + } + } + + public boolean write(String msg) + { + try + { + if (!(mmOutStream instanceof ObjectOutputStream)) + { + mmOutStream = new ObjectOutputStream(mmOutStream); + } + ((ObjectOutputStream)mmOutStream).writeUTF(msg); + } + catch (IOException e) + { + log.log(Level.SEVERE, "Exception during obj-write", e); + return false; + } + return true; + } + + /** + * Write to the connected OutStream. + * + * @param buffer The bytes to write + */ + public boolean write(byte[] buffer, int len) { + try { + mmOutStream.write(buffer, 0, len); + mmOutStream.flush(); // Ensure all data is written. + } catch (IOException e) { + log.log(Level.SEVERE, "Exception during write", e); + return false; + } + return true; + } + + public void cancel() { + try { + mmSocket.close(); + } catch (IOException e) { + log.log(Level.SEVERE, "close() of connect socket failed", e); + } + } + } +} diff --git a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/BluetoothUtil.java b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/BluetoothUtil.java new file mode 100644 index 0000000..a44e719 --- /dev/null +++ b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/BluetoothUtil.java @@ -0,0 +1,307 @@ +package com.junjunguo.pocketmaps.bluetooth; + +import android.app.Activity; +import android.app.AlertDialog; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.widget.TextView; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; +import java.io.File; +import java.util.UUID; +import java.util.logging.Logger; +import java.util.logging.Level; +import org.oscim.utils.IOUtils; + +/** + * Hints from: https://medium.com/@svaish97/sending-and-receiving-data-via-bluetooth-android-3b4a44406e84 + * @author Paul Kashofer soundmodul@gmx.at + */ +public class BluetoothUtil +{ + private final static Logger log = Logger.getLogger(BluetoothUtil.class.getName()); + public final static int MSG_FINISH = 1000; + public final static int MSG_PROGRESS = 50; + public final static int MSG_STARTED = 0; + public final static int MSG_FAILED = -1; + + public enum HeaderType { File } + + BluetoothService service; + + public static String PERMISSIONS[] = { + "android.permission.BLUETOOTH_CONNECT", + "android.permission.BLUETOOTH_ADVERTISE", + "android.permission.BLUETOOTH_SCAN", + "android.permission.BLUETOOTH_ADMIN" + }; + + public BluetoothUtil(UUID uuid, String name, Activity activity) + { + service = new BluetoothService(activity, name, uuid); + } + + public boolean isSupported() + { + return service.isSupported(); + } + + public boolean isEnabled() + { + return service.isEnabled(); + } + + public static void requestPermission(Activity activity) + { + if (android.os.Build.VERSION.SDK_INT >= 31) // android.os.Build.VERSION_CODES.S + { + //sPermission[0] = android.Manifest.permission.BLUETOOTH_CONNECT; + androidx.core.app.ActivityCompat.requestPermissions(activity, + PERMISSIONS, + getId()); + } + } + + public static boolean isPermissionAllowed(Activity activity) + { + if (android.os.Build.VERSION.SDK_INT >= 31) // android.os.Build.VERSION_CODES.S + { + for (String p : PERMISSIONS) + { + if (androidx.core.content.ContextCompat.checkSelfPermission(activity, p) == android.content.pm.PackageManager.PERMISSION_DENIED) { return false; } + } + } + return true; // Not necessary on older versions + } + + private static int curId = 0; + public static int getId() + { + curId++; + return curId; + } + + + public void requestEnable(Activity activity) + { + Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + activity.startActivityForResult(enableIntent, 0); + } + + public boolean isConnected() + { + return service.getState() == BluetoothService.STATE_CONNECTED; + } + + public void startReceiver(Handler receiver) + { + service.startReceiver(receiver); + } + + int selectedIndex = -1; + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param context The Activity context. + * @param msgHandler The handler that sends empty messages of BluetoothUtil.MSG_XXX. + */ + public void connect(Context context, Handler msgHandler) + { + Properties devices = service.getPairedDevices(); + final String choices[] = new String[devices.size()]; + int cnt = 0; + for (Object entry : service.getPairedDevices().entrySet()) + { + choices[cnt] = entry.toString(); + cnt++; + } + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle("BT devices") + .setPositiveButton("Select", (dialog, which) -> { + int idx = choices[selectedIndex].lastIndexOf("="); + if (idx < 0) { log.warning("Device list error."); } + else + { + String addr = choices[selectedIndex].substring(idx+1); + service.connect(addr, msgHandler); + } + }).setSingleChoiceItems(choices, 0, (dialog, which) -> { selectedIndex = which; }); + AlertDialog dialog = builder.create(); + dialog.show(); + + } + + /** Transmits a file. + * @param file The file to transmit. + * @param msgHandler The handler that returns the progress in percent, or MSG_FAILED, or MSG_FINISH. */ + public void transmit(File file, Handler msgHandler) + { + if (Looper.getMainLooper().getThread() == Thread.currentThread()) + { + new Thread(() -> transmitNow(file, msgHandler)).start(); + } + else + { + transmitNow(file, msgHandler); + } + } + + private void transmitNow(File file, Handler msgHandler) + { + long fsize = file.length(); + long fwritten = 0; + try (FileInputStream fis = new FileInputStream(file)) + { + byte buffer[] = new byte[1024*4]; + String msgS = createHeader(HeaderType.File, file.length(), file.getName()); + if (!service.write(msgS)) { throw new IOException("Writing-header-error."); } + while(true) + { + int len = fis.read(buffer); + if (len < 0) { break; } + if (!service.write(buffer, len)) { throw new IOException("Writing-data-error."); } + fwritten += len; + float percent = ((float)fwritten/fsize) * 100.0f; + Message msg = new Message(); + msg.what = MSG_PROGRESS; + msg.arg1 = (int)percent; + msgHandler.sendMessage(msg); + } + msgHandler.sendEmptyMessage(MSG_FINISH); + } + catch(IOException e) + { + msgHandler.sendEmptyMessage(MSG_FAILED); + e.printStackTrace(); + } + } + + public static Handler createFileReceiver(File fileDir, TextView view) + { + Handler h = new Handler() + { + boolean writeError = false; + int counter = 0; + FileOutputStream fos; + @Override public void handleMessage(Message msg) + { + if (writeError) { return; } + if (msg.what == BluetoothUtil.MSG_FAILED) + { + IOUtils.closeQuietly(fos); + view.setText("Receiving failed, try again"); + view.setTextColor(0xFFFF0000); // 0xAARRGGBB + } + else if (msg.what == BluetoothUtil.MSG_STARTED) + { + view.setText("Receiving started ..."); + view.setTextColor(0xFFFF0000); // 0xAARRGGBB + String fileName = headerGetText(msg.obj.toString(), 2); + + log.log(Level.INFO, "Writing Bluetooth data to {0}", fileName); + if (fileName == null) + { + view.setText("Error reading header"); + view.setTextColor(0xFFFF0000); // 0xAARRGGBB + writeError = true; + } + else if (fos != null) + { + view.setText("Error writing file, stream already created"); + view.setTextColor(0xFFFF0000); // 0xAARRGGBB + writeError = true; + } + else + { + log.log(Level.INFO, "Start receiving file: {0}", fileName); + try + { + fos = new FileOutputStream(new File(fileDir, fileName), false); + } + catch (IOException e) + { + e.printStackTrace(); + view.setText("Error crating file-stream"); + view.setTextColor(0xFFFF0000); // 0xAARRGGBB + writeError = true; + } + } + } + else if (msg.what == BluetoothUtil.MSG_PROGRESS) + { + view.setText("Receiving [" + counter + "][" + msg.arg1 + "%]"); + view.setTextColor(0xFF0000FF); // 0xAARRGGBB + try + { + fos.write((byte[])msg.obj, 0, msg.arg2); + } + catch (IOException e) + { + e.printStackTrace(); + view.setText("Error writing file"); + view.setTextColor(0xFFFF0000); // 0xAARRGGBB + writeError = true; + } + counter ++; + } + else if (msg.what == BluetoothUtil.MSG_FINISH) + { + IOUtils.closeQuietly(fos); + view.setText("File successfully received"); + view.setTextColor(0xFF00FF00); // 0xAARRGGBB + } + } + }; + return h; + } + + protected static String createHeader(HeaderType type, long dataLength, String ...more) + { + StringBuilder sb = new StringBuilder(); + sb.append(type.toString()).append(":").append(dataLength); + for (String m : more) { sb.append(":").append(m); } + return sb.toString(); + } + + protected static HeaderType headerGetType(String headText) + { + String typeS = headerGetText(headText, 0); + if (HeaderType.File.toString().equals(typeS)) + { + return HeaderType.File; + } + return null; + } + + protected static long headerGetDataLength(String headText) + { + String lenS = headerGetText(headText, 1); + if (lenS == null) { return -1; } + try + { + return Long.parseLong(lenS); + } + catch (NumberFormatException e) + { + e.printStackTrace(); + return -1; + } + } + + /** Returns text from header. + * @param headText the whole headText. + * @param num The array-number, where 0=HeaderType and 1=DataLength + * @return The requested text. */ + protected static String headerGetText(String headText, int num) + { + String arr[] = headText.split(":"); + if (arr.length <= num) { return null; } + return arr[num]; + } +} diff --git a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/Constants.java b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/Constants.java new file mode 100644 index 0000000..f675fe3 --- /dev/null +++ b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/bluetooth/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 com.junjunguo.pocketmaps.bluetooth; + +/** + * Defines several constants used between {@link BluetoothChatService} and the UI. + * Source: https://github.com/android/connectivity-samples + */ +public interface Constants { + + // Message types sent from the BluetoothChatService Handler + int MESSAGE_STATE_CHANGE = 1; + int MESSAGE_READ = 2; + int MESSAGE_WRITE = 3; + int MESSAGE_DEVICE_NAME = 4; + int MESSAGE_TOAST = 5; + + // Key names received from the BluetoothChatService Handler + String DEVICE_NAME = "device_name"; + String TOAST = "toast"; +} diff --git a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/downloader/MapDownloadUnzip.java b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/downloader/MapDownloadUnzip.java index b9a1a58..9734033 100644 --- a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/downloader/MapDownloadUnzip.java +++ b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/downloader/MapDownloadUnzip.java @@ -137,7 +137,6 @@ private static void broadcastReceiverCheck(Activity activity, { stUpdate.logUserThread("Unzipping: " + myMap.getMapName()); unzipBg(activity, myMap, stUpdate); - return; } else if (preStatus == DownloadManager.STATUS_FAILED) { diff --git a/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/model/listeners/ObjectRunnable.java b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/model/listeners/ObjectRunnable.java new file mode 100644 index 0000000..396f057 --- /dev/null +++ b/PocketMaps/app/src/main/java/com/junjunguo/pocketmaps/model/listeners/ObjectRunnable.java @@ -0,0 +1,6 @@ +package com.junjunguo.pocketmaps.model.listeners; + +public interface ObjectRunnable +{ + public void run(Object o); +} diff --git a/PocketMaps/app/src/main/res/layout/activity_export.xml b/PocketMaps/app/src/main/res/layout/activity_export.xml index f101d84..d4ac805 100644 --- a/PocketMaps/app/src/main/res/layout/activity_export.xml +++ b/PocketMaps/app/src/main/res/layout/activity_export.xml @@ -30,6 +30,30 @@ android:layout_gravity="left" android:text="Path" /> + + + + +