diff --git a/app/build.gradle b/app/build.gradle index 71d899451..8dc5e7243 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,7 @@ android.defaultConfig.vectorDrawables.useSupportLibrary = true dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'androidx.lifecycle:lifecycle-process:2.8.2' androidTestImplementation('androidx.test.espresso:espresso-core:3.6.1', { exclude group: 'com.android.support', module: 'support-annotations' }) diff --git a/app/src/main/kotlin/io/treehouses/remote/MainApplication.kt b/app/src/main/kotlin/io/treehouses/remote/MainApplication.kt index 1d994ec5f..d327cf75d 100644 --- a/app/src/main/kotlin/io/treehouses/remote/MainApplication.kt +++ b/app/src/main/kotlin/io/treehouses/remote/MainApplication.kt @@ -9,14 +9,21 @@ import android.content.Intent import android.content.ServiceConnection import android.os.Build import android.os.IBinder +import android.util.Log import androidx.core.app.NotificationCompat -import androidx.preference.PreferenceManager +import androidx.lifecycle.ProcessLifecycleOwner import com.parse.Parse import io.treehouses.remote.network.BluetoothChatService +import io.treehouses.remote.utils.AppLifecycleObserver +import io.treehouses.remote.utils.AppLifecycleTracker +import io.treehouses.remote.utils.GPSService import io.treehouses.remote.utils.SaveUtils class MainApplication : Application() { var logSent = false + private lateinit var appLifecycleObserver: AppLifecycleObserver + private lateinit var activityLifecycleTracker: AppLifecycleTracker +// private var bluetoothService: BluetoothChatService? = null override fun onCreate() { super.onCreate() @@ -27,22 +34,26 @@ class MainApplication : Application() { terminalList = ArrayList() tunnelList = ArrayList() commandList = ArrayList() - Parse.initialize(Parse.Configuration.Builder(this) + Parse.initialize( + Parse.Configuration.Builder(this) .applicationId(Constants.PARSE_APPLICATION_ID) .clientKey(null) .server(Constants.PARSE_URL) .build() ) SaveUtils.initCommandsList(applicationContext) + + appLifecycleObserver = AppLifecycleObserver() + ProcessLifecycleOwner.get().lifecycle.addObserver(appLifecycleObserver) + + activityLifecycleTracker = AppLifecycleTracker() + registerActivityLifecycleCallbacks(activityLifecycleTracker) } private val connection = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, service: IBinder) { - // We've bound to LocalService, cast the IBinder and get LocalService instance val binder = service as BluetoothChatService.LocalBinder mChatService = binder.service -// sendBroadcast(Intent().setAction(BLUETOOTH_SERVICE_CONNECTED)) } override fun onServiceDisconnected(arg0: ComponentName) { @@ -50,56 +61,76 @@ class MainApplication : Application() { } } - fun getCurrentBluetoothService() : BluetoothChatService? { + fun getCurrentBluetoothService(): BluetoothChatService? { + if (mChatService == null) { + mChatService = BluetoothChatService() + } return mChatService } fun startBluetoothService() { - Intent(this, BluetoothChatService::class.java).also { intent -> bindService(intent, connection, Context.BIND_AUTO_CREATE) } + Intent(this, BluetoothChatService::class.java).also { intent -> + bindService(intent, connection, Context.BIND_AUTO_CREATE) + } } fun stopBluetoothService() { - if (!PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.KEEP_BLUETOOTH_ALIVE, false)) { - try {unbindService(connection)} catch (e: Exception) {} + try { + unbindService(connection) + mChatService = null + } catch (e: Exception) { + e.printStackTrace() } + val bluetoothIntent = Intent(this, BluetoothChatService::class.java) + stopService(bluetoothIntent) } - - private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val bluetoothChannel = NotificationChannel( - getString(R.string.bt_notification_ID), - getString(R.string.bt_notification_channel), - NotificationManager.IMPORTANCE_HIGH).apply { + getString(R.string.bt_notification_ID), + getString(R.string.bt_notification_channel), + NotificationManager.IMPORTANCE_HIGH + ).apply { description = getString(R.string.bt_notification_description) lockscreenVisibility = NotificationCompat.VISIBILITY_PRIVATE } - // Register the channel with the system - val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notificationManager: NotificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannels(listOf(bluetoothChannel)) } } + fun stopAllServices() { + stopBluetoothService() + val gpsIntent = Intent(this, GPSService::class.java) + stopService(gpsIntent) + Log.d("MainApplication", "All services stopped") + } + companion object { const val BLUETOOTH_SERVICE_CONNECTED = "BLUETOOTH_SERVICE_CONNECTED" + @JvmStatic var terminalList: ArrayList? = null private set + @JvmStatic var tunnelList: ArrayList? = null private set + @JvmStatic lateinit var commandList: ArrayList private set + @JvmField var showLogDialog = true + @JvmField var ratingDialog = true - lateinit var context:Context - - var mChatService : BluetoothChatService? = null + lateinit var context: Context + var mChatService: BluetoothChatService? = null } } \ No newline at end of file diff --git a/app/src/main/kotlin/io/treehouses/remote/bases/BaseBluetoothChatService.kt b/app/src/main/kotlin/io/treehouses/remote/bases/BaseBluetoothChatService.kt index 1c383ed4a..f3df42f5f 100644 --- a/app/src/main/kotlin/io/treehouses/remote/bases/BaseBluetoothChatService.kt +++ b/app/src/main/kotlin/io/treehouses/remote/bases/BaseBluetoothChatService.kt @@ -77,14 +77,16 @@ open class BaseBluetoothChatService @JvmOverloads constructor(handler: Handler? } protected fun startNotification() { - val disconnectIntent = Intent(DISCONNECT_ACTION) - val disconnectPendingIntent: PendingIntent = PendingIntent.getBroadcast(this, 0, disconnectIntent, FLAG_IMMUTABLE) + context?.let { + val disconnectIntent = Intent(DISCONNECT_ACTION) + val disconnectPendingIntent = PendingIntent.getBroadcast(this, 0, disconnectIntent, FLAG_IMMUTABLE) - val onClickIntent = Intent(this, InitialActivity::class.java) - val pendingClickIntent = PendingIntent.getActivity(this, 0, onClickIntent, FLAG_IMMUTABLE) + val onClickIntent = Intent(this, InitialActivity::class.java) + val pendingClickIntent = PendingIntent.getActivity(this, 0, onClickIntent, FLAG_IMMUTABLE) - val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this, getString(R.string.bt_notification_ID)) - val notification: Notification = notificationBuilder.setOngoing(true) + val notificationBuilder: NotificationCompat.Builder = + NotificationCompat.Builder(this, getString(R.string.bt_notification_ID)) + val notification: Notification = notificationBuilder.setOngoing(true) .setContentTitle("Treehouses Remote is currently running") .setContentText("Connected to ${mDevice?.name}") .setPriority(NotificationCompat.PRIORITY_HIGH) @@ -93,14 +95,15 @@ open class BaseBluetoothChatService @JvmOverloads constructor(handler: Handler? .setContentIntent(pendingClickIntent) .addAction(R.drawable.bluetooth, "Disconnect", disconnectPendingIntent) .build() - ServiceCompat.startForeground(this,2, - notification, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE - } else { - 0 - } - ) + ServiceCompat.startForeground(this, 2, + notification, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE + } else { + 0 + } + ) + } } /** diff --git a/app/src/main/kotlin/io/treehouses/remote/network/BluetoothChatService.kt b/app/src/main/kotlin/io/treehouses/remote/network/BluetoothChatService.kt index f0e84332c..7e86dd575 100644 --- a/app/src/main/kotlin/io/treehouses/remote/network/BluetoothChatService.kt +++ b/app/src/main/kotlin/io/treehouses/remote/network/BluetoothChatService.kt @@ -1,20 +1,3 @@ -/* -* Copyright 2017 The Android Open Source Project, Inc. -* -* Licensed to the Apache Software Foundation (ASF) under one or more contributor -* license agreements. See the NOTICE file distributed with this work for additional -* information regarding copyright ownership. The ASF licenses this file to you 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 io.treehouses.remote.network import android.bluetooth.BluetoothDevice @@ -32,16 +15,6 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream - -/** - * Created by yubo on 7/11/17. - */ -/** - * 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. - */ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, applicationContext: Context? = null) : BaseBluetoothChatService(handler, applicationContext) { inner class DisconnectReceiver: BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -52,35 +25,21 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a } } - // Member fields - private val mBinder = LocalBinder() private var mConnectThread: ConnectThread? = null private var mConnectedThread: ConnectedThread? = null - private val receiver = DisconnectReceiver() - /** - * Start the chat service. Specifically start AcceptThread to begin a - * session in listening (server) mode. Called by the Activity onResume() - */ @Synchronized override fun start() { bNoReconnect = false - // Cancel any thread attempting to make a connection mConnectThread?.cancel() mConnectThread = null - - // Cancel any thread currently running a connection mConnectedThread?.cancel() mConnectedThread = null - - // Update UI title updateUserInterfaceTitle() } - - fun updateHandler(handler: Handler) { mHandler = handler } @@ -90,7 +49,6 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a } inner class LocalBinder : Binder() { - // Return this instance of LocalService so clients can call public methods val service: BluetoothChatService get() = this@BluetoothChatService } @@ -102,6 +60,7 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onCreate() { super.onCreate() + context = applicationContext val i = IntentFilter() i.addAction(DISCONNECT_ACTION) registerReceiver(receiver, i, RECEIVER_NOT_EXPORTED) @@ -115,79 +74,50 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a var connectedDeviceName: String = "" - - - /** - * Start the ConnectThread to initiate a connection to a remote device. - * - * @param device The BluetoothDevice to connect - * @param secure Socket Security type - Secure (true) , Insecure (false) - */ @Synchronized fun connect(device: BluetoothDevice, secure: Boolean) { - - // Cancel any thread attempting to make a connection if (state == Constants.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 = ConnectThread(device, secure) 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 - */ @Synchronized fun connected(socket: BluetoothSocket?, device: BluetoothDevice, socketType: String) { connectedDeviceName = device.name mDevice = device - // 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 } startNotification() - - // Start the thread to manage the connection and perform transmissions mConnectedThread = ConnectedThread(socket, socketType) mConnectedThread!!.start() - // Send the name of the connected device back to the UI Activity updateUserInterfaceTitle() val msg = mHandler?.obtainMessage(Constants.MESSAGE_DEVICE_NAME) val bundle = Bundle() bundle.putString(Constants.DEVICE_NAME, device.name) msg?.data = bundle mHandler?.sendMessage(msg ?: Message()) - // Update UI title } - /** - * Stop all threads - */ @Synchronized fun stop() { bNoReconnect = true @@ -202,37 +132,21 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a state = Constants.STATE_NONE stopForeground(true) - // Update UI title updateUserInterfaceTitle() } - /** - * Write to the ConnectedThread in an unsynchronized manner - * - * @param out The bytes to write - * @see ConnectedThread.write - */ fun write(out: ByteArray?) { - // Create temporary object var r: ConnectedThread? - // Synchronize a copy of the ConnectedThread synchronized(this) { if (state != Constants.STATE_CONNECTED) return r = mConnectedThread } - // Perform the write unsynchronized if (out != null) { r!!.write(out) } } - - - /** - * Indicate that the connection was lost and notify the UI Activity. - */ private fun connectionLost() { - // Send a failure message back to the Activity callHandler("Device connection was lost") stopForeground(true) @@ -241,43 +155,26 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a connect(mDevice!!, true) } else { state = Constants.STATE_NONE - // Update UI title updateUserInterfaceTitle() - // Start the service over to restart listening mode 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 inner class ConnectThread(private val mmDevice: BluetoothDevice, secure: Boolean) : Thread() { private val mmSocket: BluetoothSocket? private val mSocketType: String override fun run() { name = "ConnectThread$mSocketType" this@BluetoothChatService.state = Constants.STATE_CONNECTING - // 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 (e: Exception) { - // Close the socket closeSocket() connectionFailed() return } - - // Reset the ConnectThread because we're done synchronized(this@BluetoothChatService) { mConnectThread = null } - - // Start the connected thread connected(mmSocket, mmDevice, mSocketType) } @@ -291,13 +188,8 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a init { var tmp: BluetoothSocket? = null mSocketType = if (secure) "Secure" else "Insecure" - - // Get a BluetoothSocket for a connection with the - // given BluetoothDevice try { -// if (secure) { tmp = mmDevice.createRfcommSocketToServiceRecord(MY_UUID_SECURE) - // } else { this@BluetoothChatService.state = Constants.STATE_CONNECTING } catch (e: Exception) { e.printStackTrace() @@ -308,10 +200,6 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a } } - /** - * This thread runs during a connection with a remote device. - * It handles all incoming and outgoing transmissions. - */ private inner class ConnectedThread(socket: BluetoothSocket?, socketType: String) : Thread() { private val mmSocket: BluetoothSocket? private val mmInStream: InputStream? @@ -320,17 +208,11 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a val buffer = ByteArray(10000) var bytes: Int var out: String - - // Keep listening to the InputStream while connected while (true) { try { - // Read from the InputStream bytes = mmInStream!!.read(buffer) out = String(buffer, 0, bytes) mHandler?.obtainMessage(Constants.MESSAGE_READ, bytes, -1, out)?.sendToTarget() - // mEmulatorView.write(buffer, bytes); - // Send the obtained bytes to the UI Activity - //mHandler.obtainMessage(BlueTerm.MESSAGE_READ, bytes, -1, buffer).sendToTarget(); } catch (e: IOException) { e.printStackTrace() connectionLost() @@ -339,16 +221,9 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a } } - /** - * Write to the connected OutStream. - * - * @param buffer The bytes to write - */ fun write(buffer: ByteArray) { try { mmOutStream!!.write(buffer) - - // Share the sent message back to the UI Activity mHandler?.obtainMessage(Constants.MESSAGE_WRITE, -1, -1, buffer)?.sendToTarget() } catch (e: IOException) { e.printStackTrace() @@ -367,8 +242,6 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a mmSocket = socket var tmpIn: InputStream? = null var tmpOut: OutputStream? = null - - // Get the BluetoothSocket input and output streams try { tmpIn = socket!!.inputStream tmpOut = socket.outputStream @@ -381,14 +254,4 @@ class BluetoothChatService @JvmOverloads constructor(handler: Handler? = null, a this@BluetoothChatService.updateUserInterfaceTitle() } } - -// inner class DisconnectReceiver : BroadcastReceiver() { -// override fun onReceive(context: Context?, intent: Intent?) { -// if (intent?.action == DISCONNECT_ACTION) { -// stop() -// } -// } -// -// } - } \ No newline at end of file diff --git a/app/src/main/kotlin/io/treehouses/remote/utils/AppLifeCycleTracker.kt b/app/src/main/kotlin/io/treehouses/remote/utils/AppLifeCycleTracker.kt new file mode 100644 index 000000000..3b7764959 --- /dev/null +++ b/app/src/main/kotlin/io/treehouses/remote/utils/AppLifeCycleTracker.kt @@ -0,0 +1,39 @@ +package io.treehouses.remote.utils + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import io.treehouses.remote.MainApplication + + +class AppLifecycleTracker : ActivityLifecycleCallbacks { + private var activityReferences = 0 + private var isActivityChangingConfigurations = false + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + + override fun onActivityStarted(activity: Activity) { + if (++activityReferences == 1 && !isActivityChangingConfigurations) { + // App enters foreground + } + } + + override fun onActivityResumed(activity: Activity) {} + + override fun onActivityPaused(activity: Activity) {} + + override fun onActivityStopped(activity: Activity) { + isActivityChangingConfigurations = activity.isChangingConfigurations + if (--activityReferences == 0 && !isActivityChangingConfigurations) { + // App enters background + } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + + override fun onActivityDestroyed(activity: Activity) { + if (activityReferences == 0 && !isActivityChangingConfigurations) { + (activity.application as MainApplication).stopAllServices() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/io/treehouses/remote/utils/AppLifecycleObserver.kt b/app/src/main/kotlin/io/treehouses/remote/utils/AppLifecycleObserver.kt new file mode 100644 index 000000000..ad3b6fdc0 --- /dev/null +++ b/app/src/main/kotlin/io/treehouses/remote/utils/AppLifecycleObserver.kt @@ -0,0 +1,23 @@ +package io.treehouses.remote.utils + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner + +class AppLifecycleObserver : DefaultLifecycleObserver { + private var activityReferences = 0 + private var isActivityChangingConfigurations = false + + override fun onStart(owner: LifecycleOwner) { + if (++activityReferences == 1 && !isActivityChangingConfigurations) { + // App enters foreground + } + } + + override fun onStop(owner: LifecycleOwner) { + isActivityChangingConfigurations = owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) + if (--activityReferences == 0 && !isActivityChangingConfigurations) { + // App enters background + } + } +} \ No newline at end of file