Skip to content

Commit

Permalink
* make HostService actually a Service
Browse files Browse the repository at this point in the history
  • Loading branch information
Ink committed Apr 19, 2024
1 parent 18b85b9 commit d1c5df9
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 44 deletions.
4 changes: 4 additions & 0 deletions glass-ee/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service
android:name=".core.HostService"
android:enabled="true"
android:exported="true" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.damn.anotherglass.glass.ee.host.core

import android.content.Context
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import com.damn.anotherglass.shared.gps.GPSServiceAPI
Expand All @@ -9,66 +12,104 @@ import com.damn.anotherglass.shared.rpc.RPCMessage
import com.damn.anotherglass.shared.rpc.RPCMessageListener
import com.damn.glass.shared.gps.MockGPS

// lets pretend for now its actually a Service
class HostService(private val context: Context) {

interface IService {

enum class ServiceState {
INITIALIZING,
WAITING,
CONNECTED,
DISCONNECTED
}

val state: ServiceState // todo: LiveData
}

class HostService() : Service(), IService {

private val client = WiFiClient()

private val gps = MockGPS(context)
private lateinit var gps: MockGPS

fun start() {
inner class LocalBinder : Binder() {
fun getService(): IService = this@HostService
}

private val _binder = LocalBinder()

override var state: IService.ServiceState = IService.ServiceState.INITIALIZING
private set

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
return START_STICKY
}

@Override
override fun onCreate() {
super.onCreate()
gps = MockGPS(this)
start()
}

@Override
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "HostService stopped")
gps.remove()
client.stop()
}

private fun start() {
Log.i(TAG, "HostService started")

val listener: RPCMessageListener = object : RPCMessageListener {
override fun onWaiting() {
Log.d(TAG, "Waiting")
state = IService.ServiceState.WAITING
}

override fun onConnectionStarted(device: String) {
Log.d(TAG, "Connected to $device")
state = IService.ServiceState.CONNECTED
}

override fun onDataReceived(data: RPCMessage) {
when(data.service) {
when (data.service) {
GPSServiceAPI.ID -> {
Log.d(TAG, "GPS data received")
if (data.type.equals(Location::class.java.getName()))
gps.publish(data.payload as Location)
}

else -> Log.e(TAG, "Unknown service: ${data.service}")
}
}

override fun onConnectionLost(error: String?) {
Log.e(TAG, "onConnectionLost: $error");
state = IService.ServiceState.DISCONNECTED
}

override fun onShutdown() {
Log.d(TAG, "onShutdown")
stopSelf()
}
}
try {
gps.start()
} catch (e: SecurityException) {
// Requires MOCK_LOCATION permission given through ADB
// `adb shell appops set <id> android:mock_location allow`
// where <uid> is from exception message
// `java.lang.SecurityException: com.damn.anotherglass.glass.ee from uid 10063 not allowed to perform MOCK_LOCATION`
// or `adb shell appops set com.damn.anotherglass.glass.ee android:mock_location allow`
Log.e(TAG, "GPS mocking is not enabled: $e")
Toast.makeText(
context,
this,
"GPS provider not available, please enable location mocking using ADB or developer settings",
Toast.LENGTH_LONG
).show()
}
client.start(context, listener)
client.start(this, listener)
}

fun stop() {
Log.i(TAG, "HostService stopped")
gps.remove()
client.stop()
}
override fun onBind(intent: Intent?): IBinder? = _binder

companion object {
private const val TAG = "HostService"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,83 @@
package com.damn.anotherglass.glass.ee.host.ui

import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.viewpager.widget.ViewPager
import com.damn.anotherglass.glass.ee.host.R
import com.damn.anotherglass.glass.ee.host.core.HostService
import com.damn.anotherglass.glass.ee.host.core.IService
import com.damn.anotherglass.glass.ee.host.gpsPermissions
import com.damn.anotherglass.glass.ee.host.ui.cards.BaseFragment
import com.damn.anotherglass.glass.ee.host.ui.cards.MapCard
import com.damn.anotherglass.glass.ee.host.ui.cards.TextLayoutFragment
import com.damn.anotherglass.glass.ee.host.utility.hasPermission
import com.damn.anotherglass.glass.ee.host.utility.isRunning
import com.example.glass.ui.GlassGestureDetector
import com.google.android.material.tabs.TabLayout

/**
* Main activity of the application. It provides viewPager to move between fragments.
*/

// TODO:
// - add connect/disconnect action on tap
// - add GPS permissions/enable mock/enable/disable menu on top of MapCard
// (remove location permissions requirement for service start)
// - add option to connect by barcode
// - add Bluetooth connection support (and WiFi for xe)?
// - add zoom levels to map card
// - add controls cards: slider, Gyro lists
// - add notifications card
// - extract string constants

class MainActivity : BaseActivity() {

private val connection = GlassServiceConnection()

private val fragments: MutableList<BaseFragment> = ArrayList()
private lateinit var viewPager: ViewPager

private lateinit var client: HostService

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.view_pager_layout)
viewPager = findViewById(R.id.viewPager)
fragments.add(TextLayoutFragment.newInstance("Initializing…", "", "", null))

fragments.add(MapCard.newInstance())
viewPager.setAdapter(ScreenSlidePagerAdapter(supportFragmentManager))
viewPager = findViewById(R.id.viewPager)
viewPager.isSaveFromParentEnabled = false
viewPager.setAdapter(object : FragmentStatePagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment = fragments[position]
override fun getCount(): Int = fragments.size
override fun getItemPosition(o: Any): Int = POSITION_NONE // TODO: hack
})

val tabLayout = findViewById<TabLayout>(R.id.page_indicator)
tabLayout.setupWithViewPager(viewPager, true)

// todo: must be a service
client = HostService(this)

tryStartService()
}

override fun onResume() {
super.onResume()
// do not bind if service is not running to avoid starting it
if(isRunning<HostService>())
connection.bindGlassService()
}

override fun onPause() {
super.onPause()
connection.unbindGlassService()
}

private fun tryStartService() {
// todo: check if we have wifi connection, and it looks like tethering one
// todo: pass address to service
if (!checkLocationPermission()) return
client.start()
startService(Intent(this, HostService::class.java))
connection.bindGlassService()
}

private fun checkLocationPermission(): Boolean {
Expand All @@ -70,7 +99,7 @@ class MainActivity : BaseActivity() {
when (requestCode) {
PERMISSIONS_REQUEST_LOCATION -> {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
client.start()
tryStartService()
} else {
Log.e(TAG, "Location permission denied")
// todo: show missing permission message
Expand All @@ -80,32 +109,82 @@ class MainActivity : BaseActivity() {
}
}

override fun onDestroy() {
super.onDestroy()
client.stop()
}

override fun onGesture(gesture: GlassGestureDetector.Gesture): Boolean =
when (gesture) {
GlassGestureDetector.Gesture.TAP -> {
fragments[viewPager.currentItem].onSingleTapUp()
true
}

GlassGestureDetector.Gesture.SWIPE_UP, // for emulator
GlassGestureDetector.Gesture.TWO_FINGER_SWIPE_DOWN -> {
stopService(Intent(this, HostService::class.java))
true
}

else -> super.onGesture(gesture)
}

private inner class ScreenSlidePagerAdapter(fm: FragmentManager) :
FragmentStatePagerAdapter(fm) {
private inner class GlassServiceConnection : ServiceConnection {
var service: IService? = null
private set
private var bound = false

override fun getItem(position: Int): Fragment = fragments[position]
fun bindGlassService() {
try {
if (bound) {
unbindService(connection)
}
} catch (e: Exception) {
e.printStackTrace()
}
bound = bindService(Intent(this@MainActivity, HostService::class.java), connection, 0)
}

fun unbindGlassService() {
if (!bound) return
bound = false
service = null
unbindService(connection)
}

override fun onServiceConnected(name: ComponentName, service: IBinder) {
this.service = (service as HostService.LocalBinder).getService()
updateUI()
}

override fun onServiceDisconnected(name: ComponentName) {
service = null
unbindGlassService()
updateUI()
}
}

private fun updateUI() = when (connection.service) {
null -> serviceNotRunningState()
else -> serviceRunningState()
}

override fun getCount(): Int = fragments.size
private fun serviceRunningState() {
// todo: I can't actually just replace the fragments directly, but I hack it for now
fragments.clear()
fragments.add(
TextLayoutFragment.newInstance(
"Connected to the service. Swipe down with two fingers to stop the service", "", ""
)
)
fragments.add(MapCard.newInstance())
viewPager.adapter?.notifyDataSetChanged()
}

private fun serviceNotRunningState() {
fragments.clear()
fragments.add(TextLayoutFragment.newInstance("Service was shut down", "", ""))
viewPager.adapter?.notifyDataSetChanged()
}

companion object {
private const val TAG = "MainActivity"
private const val PERMISSIONS_REQUEST_LOCATION = 1

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.damn.anotherglass.glass.ee.host.utility

import android.app.ActivityManager
import android.app.Service
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
Expand All @@ -14,3 +16,11 @@ fun Context.hasPermission(permission: String): Boolean =

fun Context.locationManager() =
getSystemService(AppCompatActivity.LOCATION_SERVICE) as LocationManager

inline fun <reified T : Service> Context.isRunning(): Boolean {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val services = activityManager.getRunningServices(Int.MAX_VALUE)
val pkgname = packageName // in reality we will only see our own services anyway on Android 8+
val srvname: String = T::class.java.getName()
return services.any { pkgname == it.service.packageName && srvname == it.service.className && it.started }
}

0 comments on commit d1c5df9

Please sign in to comment.