diff --git a/app/build.gradle b/app/build.gradle index 1f52d24..f247b21 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,6 +48,8 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.animation:animation' implementation 'androidx.compose.runtime:runtime' + implementation 'com.google.dagger:hilt-android:2.52' + implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.7.0' diff --git a/app/src/main/java/me/itissid/privyloci/HomeFragment.kt b/app/src/main/java/me/itissid/privyloci/HomeFragment.kt deleted file mode 100644 index 2e0f319..0000000 --- a/app/src/main/java/me/itissid/privyloci/HomeFragment.kt +++ /dev/null @@ -1,64 +0,0 @@ -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import me.itissid.privyloci.ThirdPartyAppAdapter -import me.itissid.privyloci.R -import me.itissid.privyloci.UserSubscriptionAdapter -import me.itissid.privyloci.datamodels.AppContainer -import me.itissid.privyloci.datamodels.Subscription - -class HomeFragment() : Fragment() { - - private lateinit var appContainers: List - private lateinit var userSubscriptions: List - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Retrieve arguments from the Bundle - arguments?.let { - appContainers = it.getParcelableArrayList(ARG_APP_CONTAINERS) ?: emptyList() - userSubscriptions = it.getParcelableArrayList(ARG_USER_SUBSCRIPTIONS) ?: emptyList() - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - val view = inflater.inflate(R.layout.fragment_home, container, false) - - // Find the RecyclerView in the fragment's layout - val appRecyclerView = view.findViewById(R.id.app_recycler_view) - - // Set up the RecyclerView with a layout manager and the adapter for App subscriptions - appRecyclerView.layoutManager = LinearLayoutManager(context) - appRecyclerView.adapter = ThirdPartyAppAdapter(appContainers) - - // If you want to handle user subscriptions separately, set up another RecyclerView here - val userRecyclerView = view.findViewById(R.id.user_recycler_view) - userRecyclerView.layoutManager = LinearLayoutManager(context) - userRecyclerView.adapter = UserSubscriptionAdapter(userSubscriptions) - - return view - } - - companion object { - private const val ARG_APP_CONTAINERS = "app_containers" - private const val ARG_USER_SUBSCRIPTIONS = "user_subscriptions" - - @JvmStatic - fun newInstance(appContainers: ArrayList, userSubscriptions: ArrayList) = - HomeFragment().apply { - arguments = Bundle().apply { - putParcelableArrayList(ARG_APP_CONTAINERS, appContainers) - putParcelableArrayList(ARG_USER_SUBSCRIPTIONS, userSubscriptions) - } - } - } -} diff --git a/app/src/main/java/me/itissid/privyloci/LocationPermissionState.kt b/app/src/main/java/me/itissid/privyloci/LocationPermissionState.kt new file mode 100644 index 0000000..e8bb328 --- /dev/null +++ b/app/src/main/java/me/itissid/privyloci/LocationPermissionState.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * 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 + * + * https://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 me.itissid.privyloci + +import android.Manifest.permission +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat + +private val locationPermissions = + arrayOf(permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION) + +internal fun Context.hasPermission(permission: String): Boolean = + ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED + +internal fun Activity.shouldShowRationaleFor(permission: String): Boolean = + ActivityCompat.shouldShowRequestPermissionRationale(this, permission) + +/** + * State holder for location permissions. Properties are implemented as State objects so that they + * trigger a recomposition when the value changes (if the value is read within a Composable scope). + * This also implements the behavior for requesting location permissions and updating the internal + * state afterward. + * + * This class should be initialized in `onCreate()` of the Activity. Sample usage: + * + * ``` + * override fun onCreate(savedInstanceState: Bundle?) { + * super.onCreate(savedInstanceState) + * + * val locationPermissionState = LocationPermissionState(this) { permissionState -> + * if (permissionState.accessFineLocationGranted) { + * // Do something requiring precise location permission + * } + * } + * + * setContent { + * Button( + * onClick = { locationPermissionState.requestPermissions() } + * ) { + * Text("Click Me") + * } + * } + * } + * ``` + */ +class LocationPermissionState( + private val activity: ComponentActivity, + private val onResult: (LocationPermissionState) -> Unit +) { + + /** Whether permission was granted to access approximate location. */ + var accessCoarseLocationGranted by mutableStateOf(false) + private set + + /** Whether to show a rationale for permission to access approximate location. */ + var accessCoarseLocationNeedsRationale by mutableStateOf(false) + private set + + /** Whether permission was granted to access precise location. */ + var accessFineLocationGranted by mutableStateOf(false) + private set + + /** Whether to show a rationale for permission to access precise location. */ + var accessFineLocationNeedsRationale by mutableStateOf(false) + private set + + /** + * Whether to show a degraded experience (set after the permission is denied). + */ + var showDegradedExperience by mutableStateOf(false) + private set + + private val permissionLauncher: ActivityResultLauncher> = + activity.registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { + updateState() + showDegradedExperience = !hasPermission() + onResult(this) + } + + init { + updateState() + } + + private fun updateState() { + accessCoarseLocationGranted = activity.hasPermission(permission.ACCESS_COARSE_LOCATION) + accessCoarseLocationNeedsRationale = + activity.shouldShowRationaleFor(permission.ACCESS_COARSE_LOCATION) + accessFineLocationGranted = activity.hasPermission(permission.ACCESS_FINE_LOCATION) + accessFineLocationNeedsRationale = + activity.shouldShowRationaleFor(permission.ACCESS_FINE_LOCATION) + } + + /** + * Launch the permission request. Note that this may or may not show the permission UI if the + * permission has already been granted or if the user has denied permission multiple times. + */ + fun requestPermissions() { + permissionLauncher.launch(locationPermissions) + } + + fun hasPermission(): Boolean = accessCoarseLocationGranted || accessFineLocationGranted + + fun shouldShowRationale(): Boolean = !hasPermission() && ( + accessCoarseLocationNeedsRationale || accessFineLocationNeedsRationale) +} diff --git a/app/src/main/java/me/itissid/privyloci/MainActivity.kt b/app/src/main/java/me/itissid/privyloci/MainActivity.kt index fc9bbd1..ec40411 100644 --- a/app/src/main/java/me/itissid/privyloci/MainActivity.kt +++ b/app/src/main/java/me/itissid/privyloci/MainActivity.kt @@ -1,26 +1,17 @@ package me.itissid.privyloci -import HomeFragment -import PlacesTagFragment -import android.content.Context -import android.content.pm.PackageManager import android.content.res.Configuration import android.os.Build import android.os.Bundle -import android.system.Os.open -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ContextThemeWrapper +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold @@ -30,24 +21,11 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.currentComposer -import androidx.compose.runtime.currentCompositionLocalContext import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.core.view.WindowCompat -import androidx.fragment.app.Fragment import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import com.google.android.material.bottomnavigation.BottomNavigationView -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import androidx.navigation.compose.composable import androidx.compose.material.icons.filled.Home @@ -55,25 +33,18 @@ import androidx.compose.material.icons.filled.Place import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation.compose.NavHost -import kotlinx.coroutines.withContext import me.itissid.privyloci.data.DataProvider - import me.itissid.privyloci.datamodels.AppContainer import me.itissid.privyloci.datamodels.PlaceTag import me.itissid.privyloci.datamodels.Subscription import me.itissid.privyloci.datamodels.SubscriptionType -import me.itissid.privyloci.service.startMyForegroundService import me.itissid.privyloci.ui.HomeScreen import me.itissid.privyloci.util.Logger -import me.itissid.privyloci.ui.LocationPermissionScreen import me.itissid.privyloci.ui.PlacesAndAssetsScreen import me.itissid.privyloci.ui.theme.PrivyLociTheme -import java.io.BufferedReader -import java.io.File -import java.io.FileReader -import java.io.InputStreamReader +import dagger.hilt.android.AndroidEntryPoint // TODO(Sid): Replace with real data after demo. data class MockData( @@ -81,199 +52,48 @@ data class MockData( val assets: List, val subscriptions: List ) +// move to jetpack compose +@AndroidEntryPoint +class MainActivity : ComponentActivity() { -class MainActivity : AppCompatActivity() { - - - private lateinit var appContainers: List - private lateinit var userSubscriptions: List - private lateinit var places: List - private lateinit var toolbarTitle: TextView override fun onCreate(savedInstanceState: Bundle?) { - WindowCompat.setDecorFitsSystemWindows(window, false) - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) // Ensure this line is present - - // TODO(Sid): Remove Load data from JSON. - val (placesList, assetsList, subscriptionsList) = loadDataFromJson() - appContainers = processAppContainers(subscriptionsList) - userSubscriptions = subscriptionsList.filter { it.type == SubscriptionType.USER } - - places = placesList + assetsList // Combine places and assets into one list - - toolbarTitle = findViewById(R.id.toolbar_title) - // Set up bottom navigation - val bottomNavigationView: BottomNavigationView = findViewById(R.id.bottom_navigation) - bottomNavigationView.setOnNavigationItemSelectedListener { item -> - when (item.itemId) { - R.id.nav_home -> { - val fragment = HomeFragment.newInstance(ArrayList(appContainers), ArrayList(userSubscriptions)) - replaceFragment(fragment, getString(R.string.home)) - true - } - - R.id.nav_places -> { - val fragment = PlacesTagFragment.newInstance(ArrayList(places)) - replaceFragment(fragment, getString(R.string.places_assets)) - true - } - - else -> false + val locationPermissionState = LocationPermissionState(this) { + if (it.hasPermission()) { + Logger.w("MainActivity", "Location permission granted") + } else { + Logger.w("MainActivity", "Location permission denied") } } - - // Load the default fragment (Home) when the app starts - if (savedInstanceState == null) { - bottomNavigationView.selectedItemId = R.id.nav_home - } - // Start the foreground service if there are subscriptions that require sensors. - // Check and request notification permission - if (!checkNotificationPermission()) { - requestNotificationPermission() - } else { - // Permission is already granted, start the service - // TODO(Sid): Check and start all services for managing subscriptions. - if (appContainers.isNotEmpty() || userSubscriptions.isNotEmpty()) { - userSubscriptions.find { subscription -> subscription.isValid() && subscription.isTypeLocation()}?.let { - startMyForegroundService(this) - Logger.d("MainActivity", "Some valid location subscriptions found") - }?.run{ Logger.d("MainActivity", "Some valid location subscriptions found") } - } - } - } - - private fun replaceFragment(fragment: Fragment, title: String) { - toolbarTitle.text = title - - supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, fragment) - .commit() - } - - private fun loadDataFromJson(): Triple, List, List> { - val jsonString = assets.open("mock_data.json").bufferedReader().use { it.readText() } - return parseJsonToData(jsonString) - } - - - private fun processAppContainers(subscriptions: List): List { - val appMap = mutableMapOf>() - - for (subscription in subscriptions) { - if (subscription.type == SubscriptionType.APP) { - val appInfo = Gson().fromJson(subscription.appInfo, AppInfo::class.java) - appMap.getOrPut(appInfo.appName) { mutableListOf() }.add(subscription) - } - } - - return appMap.map { (appName, appSubscriptions) -> - // FIXME(Sid): The num of unique is by placeTagId and also the event type. - val uniquePlaces = appSubscriptions.map { it.placeTagId }.distinct().count() - val uniqueSubscriptions = appSubscriptions.size - AppContainer( - name = appName, - uniquePlaces = uniquePlaces, - uniqueSubscriptions = uniqueSubscriptions, - subscriptions = appSubscriptions - ) - } - } - // Foreground service permission related code - companion object { - const val REQUEST_CODE_POST_NOTIFICATIONS = 1001 - } - private fun checkNotificationPermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ContextCompat.checkSelfPermission( - this, - android.Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - } else { - // Permission is automatically granted on devices lower than Android 13 - true - } - } - private fun requestNotificationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ActivityCompat.requestPermissions( - this, - arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), - REQUEST_CODE_POST_NOTIFICATIONS - ) - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - when (requestCode) { - REQUEST_CODE_POST_NOTIFICATIONS -> { - if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { - // Permission granted, you can start your foreground service here - startMyForegroundService(this) - } else { - // Permission denied, you can show a message or handle accordingly - Logger.w("MainActivity", "POST_NOTIFICATIONS permission denied") - } + val (placesList, assetsList, subscriptionsList) = DataProvider.getData() + val places = placesList + assetsList + val userSubscriptions = subscriptionsList.filter { it.type == SubscriptionType.USER } + val appContainers = DataProvider.processAppContainers(subscriptionsList) + setContent { + PrivyLociTheme { + MainScreen( + appContainers = appContainers, + userSubscriptions = userSubscriptions, + places = places + ) + } } } - } - } -private fun parseJsonToData(jsonString: String): Triple, List, List> { - val gson = Gson() - - // Define the type tokens for each section of the JSON - val placeTagListType = object : TypeToken>() {}.type - val subscriptionListType = object : TypeToken>() {}.type - - // Parse the JSatN - val jsonData = gson.fromJson>(jsonString, Map::class.java) - - // Extract and parse places - val placesJson = gson.toJson(jsonData["places"]) - val places: List = gson.fromJson(placesJson, placeTagListType) - - // Extract and parse assets - val assetsJson = gson.toJson(jsonData["assets"]) - val assets: List = gson.fromJson(assetsJson, placeTagListType) - - // Extract and parse subscriptions - val subscriptionsJson = gson.toJson(jsonData["subscriptions"]) - val subscriptions: List = gson.fromJson(subscriptionsJson, subscriptionListType) - - return Triple(places, assets, subscriptions) -} @Preview(showBackground = true, widthDp = 320, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(showBackground = true, widthDp = 320, heightDp = 640) @Composable fun PlacesAndAssetScreenPreview() { PrivyLociTheme { - val context = LocalContext.current - val jsonString = readAssetFile(context, "mock_data.json") - - val (placesList, assetsList, subscriptionsList) = parseJsonToData(jsonString) - PlacesAndAssetsScreen(placesList) - } -} -fun readAssetFile(context: Context, fileName: String): String { - return context.assets.open(fileName).use { inputStream -> - BufferedReader(InputStreamReader(inputStream)).use { bufferedReader -> - bufferedReader.readText() - } + val (placesList, assetsList) = DataProvider.getData() + PlacesAndAssetsScreen(placesList+assetsList) } } - @Composable fun MainScreen( appContainers: List, @@ -290,8 +110,6 @@ fun MainScreen( startDestination = NavItem.Home.route, modifier = Modifier.padding(innerPadding) ) { -// composable("home") { HomeScreen(appContainers, userSubscriptions) } -// composable("places") { PlacesAndAssetsScreen(places) } composable(NavItem.Home.route) { HomeScreen(appContainers, userSubscriptions) } diff --git a/app/src/main/java/me/itissid/privyloci/PlacesAdapter.kt b/app/src/main/java/me/itissid/privyloci/PlacesAdapter.kt deleted file mode 100644 index d094284..0000000 --- a/app/src/main/java/me/itissid/privyloci/PlacesAdapter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package me.itissid.privyloci - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.card.MaterialCardView -import me.itissid.privyloci.datamodels.PlaceTag - -class PlacesAdapter(private val places: List) : - RecyclerView.Adapter() { - - class PlacesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val placeName: TextView = itemView.findViewById(R.id.place_name) - val placeType: TextView = itemView.findViewById(R.id.place_type) - val cardView: MaterialCardView = itemView.findViewById(R.id.place_card_view) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlacesViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.place_card, parent, false) - return PlacesViewHolder(view) - } - - override fun onBindViewHolder(holder: PlacesViewHolder, position: Int) { - val place = places[position] - holder.placeName.text = place.name - holder.placeType.text = place.type.name // Display the enum name (PLACE, ASSET, etc.) - - // Handle card click, you might want to open a detailed view or perform a CRUD operation - holder.cardView.setOnClickListener { - // Perform an action like showing details or editing the place - } - } - - override fun getItemCount() = places.size -} \ No newline at end of file diff --git a/app/src/main/java/me/itissid/privyloci/PlacesTagFragment.kt b/app/src/main/java/me/itissid/privyloci/PlacesTagFragment.kt deleted file mode 100644 index 3d86a67..0000000 --- a/app/src/main/java/me/itissid/privyloci/PlacesTagFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import me.itissid.privyloci.PlacesAdapter -import me.itissid.privyloci.R -import me.itissid.privyloci.datamodels.PlaceTag - -class PlacesTagFragment() : Fragment() { - private lateinit var places: List - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Retrieve arguments from the Bundle - arguments?.let { - places = it.getParcelableArrayList(ARG_PLACES) ?: emptyList() - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - val view = inflater.inflate(R.layout.fragment_places, container, false) - - // Find the RecyclerView in the fragment's layout - val recyclerView = view.findViewById(R.id.places_recycler_view) - - // Set up the RecyclerView with a layout manager and the adapter - recyclerView.layoutManager = LinearLayoutManager(context) - recyclerView.adapter = PlacesAdapter(places) - - return view - } - - companion object { - private const val ARG_PLACES = "places" - - @JvmStatic - fun newInstance(arrayList: ArrayList) = - PlacesTagFragment().apply{ - arguments = Bundle().apply { - putParcelableArrayList(ARG_PLACES, arrayList) - } - } - } -} diff --git a/app/src/main/java/me/itissid/privyloci/SubscriptionAdapter.kt b/app/src/main/java/me/itissid/privyloci/SubscriptionAdapter.kt deleted file mode 100644 index bf6779e..0000000 --- a/app/src/main/java/me/itissid/privyloci/SubscriptionAdapter.kt +++ /dev/null @@ -1,63 +0,0 @@ -package me.itissid.privyloci - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import me.itissid.privyloci.datamodels.EventType -import me.itissid.privyloci.datamodels.Subscription -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName - - -class SubscriptionAdapter( - private val subscriptions: List, - private val isUserSubscription: Boolean // Differentiates between user and app subscriptions -) : RecyclerView.Adapter() { - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val name: TextView = itemView.findViewById(R.id.tile_name) - val subscriptionInfo: TextView = itemView.findViewById(R.id.tile_subscription_count) - val lastUpdated: TextView = itemView.findViewById(R.id.tile_last_updated) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.subscription_tile, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val subscription = subscriptions[position] - if (isUserSubscription) { - // For user subscriptions (Places/Assets) - holder.name.text = subscription.placeTagId.toString() // Replace with actual place/asset name lookup - holder.subscriptionInfo.text = when (subscription.eventType) { - EventType.GEOFENCE_ENTRY -> "Entry Alert" - EventType.GEOFENCE_EXIT -> "Exit Alert" - EventType.TRACK_BLE_ASSET_DISCONNECTED -> "Location Tracked after Disconnection" // TODO(Sid): - EventType.TRACK_BLE_ASSET_NEARBY -> "Tracking when in range, but not connected" - EventType.QIBLA_DIRECTION_PRAYER_TIME -> "Direction to Kibla" - EventType.DISPLAY_PINS_MAP_TILE -> "Displaying Pins on Map" - else -> "Unknown Event" - } - holder.lastUpdated.text = "Last Updated: ${subscription.createdAt}" // Convert timestamp to date - } else { - // For app subscriptions - val appInfo = Gson().fromJson(subscription.appInfo, AppInfo::class.java) - holder.name.text = appInfo.appName - holder.subscriptionInfo.text = "Active Subscriptions: ${subscriptions.size}" - holder.lastUpdated.text = "Last Updated: ${subscription.createdAt}" // Convert timestamp to date - } - } - - - override fun getItemCount() = subscriptions.size -} - -// Example AppInfo data class for parsing the appInfo JSON -data class AppInfo( - @SerializedName("app_name") val appName: String, - @SerializedName("app_id") val appId: String -) diff --git a/app/src/main/java/me/itissid/privyloci/ThirdPartyAppAdapter.kt b/app/src/main/java/me/itissid/privyloci/ThirdPartyAppAdapter.kt deleted file mode 100644 index b92a092..0000000 --- a/app/src/main/java/me/itissid/privyloci/ThirdPartyAppAdapter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package me.itissid.privyloci - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import me.itissid.privyloci.datamodels.AppContainer - -class ThirdPartyAppAdapter(private val apps: List) : - RecyclerView.Adapter() { - - class AppViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val appName: TextView = itemView.findViewById(R.id.app_name) - val menuMore: ImageView = itemView.findViewById(R.id.menu_more) - val subscriptionDetails: TextView = itemView.findViewById(R.id.subscription_details) - val subscriptionRecyclerView: RecyclerView = itemView.findViewById(R.id.subscription_recycler_view) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.app_card, parent, false) - return AppViewHolder(view) - } - - override fun onBindViewHolder(holder: AppViewHolder, position: Int) { - val app = apps[position] - holder.appName.text = app.name - holder.subscriptionDetails.text = "${app.uniquePlaces} Places, ${app.uniqueSubscriptions} Subscriptions" - - // Handle the menu click (more options) - holder.menuMore.setOnClickListener { - // Show menu with options to delete or pause all subscriptions - } - - // Handle card click to expand and show subscriptions - holder.itemView.setOnClickListener { - val isExpanded = app.isExpanded - app.isExpanded = !isExpanded - holder.subscriptionRecyclerView.visibility = if (app.isExpanded) View.VISIBLE else View.GONE - } - - // Set up the nested RecyclerView with SubscriptionAdapter - if (app.isExpanded) { - holder.subscriptionRecyclerView.layoutManager = LinearLayoutManager(holder.itemView.context) - holder.subscriptionRecyclerView.adapter = SubscriptionAdapter(app.subscriptions, false) - holder.subscriptionRecyclerView.visibility = View.VISIBLE - } else { - holder.subscriptionRecyclerView.visibility = View.GONE - } - } - - override fun getItemCount() = apps.size -} diff --git a/app/src/main/java/me/itissid/privyloci/UserSubscriptionAdapter.kt b/app/src/main/java/me/itissid/privyloci/UserSubscriptionAdapter.kt deleted file mode 100644 index 2046bbb..0000000 --- a/app/src/main/java/me/itissid/privyloci/UserSubscriptionAdapter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package me.itissid.privyloci - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import me.itissid.privyloci.R -import me.itissid.privyloci.datamodels.Subscription - -class UserSubscriptionAdapter(private val subscriptions: List) : - RecyclerView.Adapter() { - - class UserSubscriptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val placeName: TextView = itemView.findViewById(R.id.place_name) - val eventType: TextView = itemView.findViewById(R.id.event_type) - val dateTime: TextView = itemView.findViewById(R.id.date_time) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserSubscriptionViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.user_subscription_card, parent, false) - return UserSubscriptionViewHolder(view) - } - - override fun onBindViewHolder(holder: UserSubscriptionViewHolder, position: Int) { - val subscription = subscriptions[position] - holder.placeName.text = "Place ID: ${subscription.placeTagId}" //TODO(Sid): Replace with actual place name when hooking in the data model - holder.eventType.text = subscription.eventType.name - holder.dateTime.text = subscription.formattedDate // Format as needed - } - - override fun getItemCount() = subscriptions.size -} diff --git a/app/src/main/java/me/itissid/privyloci/data/DataProvider.kt b/app/src/main/java/me/itissid/privyloci/data/DataProvider.kt index 2b35ca1..47da3bb 100644 --- a/app/src/main/java/me/itissid/privyloci/data/DataProvider.kt +++ b/app/src/main/java/me/itissid/privyloci/data/DataProvider.kt @@ -5,6 +5,7 @@ import com.google.gson.reflect.TypeToken import me.itissid.privyloci.datamodels.* import java.lang.reflect.Type +// A temporary data provider for testing the application. object DataProvider { private val gson = Gson() diff --git a/app/src/main/java/me/itissid/privyloci/datamodels/DataModels.kt b/app/src/main/java/me/itissid/privyloci/datamodels/DataModels.kt index 39b525d..03e29eb 100644 --- a/app/src/main/java/me/itissid/privyloci/datamodels/DataModels.kt +++ b/app/src/main/java/me/itissid/privyloci/datamodels/DataModels.kt @@ -31,6 +31,7 @@ data class Subscription( val subscriptionId: Int, val type: SubscriptionType, // enum {APP, USER} val placeTagId: String, + val placeTagName: String, val appInfo: String, // JSON string with app details if type is APP val createdAt: Long, val isActive: Boolean, diff --git a/app/src/main/java/me/itissid/privyloci/MainScreenPreview.kt b/app/src/main/java/me/itissid/privyloci/ui/MainScreenPreview.kt similarity index 73% rename from app/src/main/java/me/itissid/privyloci/MainScreenPreview.kt rename to app/src/main/java/me/itissid/privyloci/ui/MainScreenPreview.kt index 30d8908..dcd7a58 100644 --- a/app/src/main/java/me/itissid/privyloci/MainScreenPreview.kt +++ b/app/src/main/java/me/itissid/privyloci/ui/MainScreenPreview.kt @@ -1,13 +1,9 @@ -package me.itissid.privyloci +package me.itissid.privyloci.ui import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import me.itissid.privyloci.MainScreen import me.itissid.privyloci.data.DataProvider -import me.itissid.privyloci.datamodels.AppContainer -import me.itissid.privyloci.datamodels.PlaceTag -import me.itissid.privyloci.datamodels.Subscription import me.itissid.privyloci.datamodels.SubscriptionType diff --git a/app/src/main/java/me/itissid/privyloci/ui/SubscriptionCards.kt b/app/src/main/java/me/itissid/privyloci/ui/SubscriptionCards.kt index 01e4893..b6e7159 100644 --- a/app/src/main/java/me/itissid/privyloci/ui/SubscriptionCards.kt +++ b/app/src/main/java/me/itissid/privyloci/ui/SubscriptionCards.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import me.itissid.privyloci.datamodels.AppContainer +import me.itissid.privyloci.datamodels.EventType import me.itissid.privyloci.datamodels.PlaceTag import me.itissid.privyloci.datamodels.Subscription import java.text.SimpleDateFormat @@ -119,12 +120,20 @@ fun SubscriptionCard(subscription: Subscription) { Column(modifier = Modifier.padding(16.dp)) { // Place Name (Replace with actual place name if available) Text( - text = "Place ID: ${subscription.placeTagId}", + text = "${subscription.placeTagName}(id: ${subscription.placeTagId})", style = MaterialTheme.typography.headlineSmall ) // Event Type Text( - text = subscription.eventType.name, + text = when(subscription.eventType) { + EventType.GEOFENCE_ENTRY-> "Entry Alert" + EventType.GEOFENCE_EXIT -> "Exit Alert" + EventType.TRACK_BLE_ASSET_DISCONNECTED -> "Location Tracked after Disconnection" + EventType.TRACK_BLE_ASSET_NEARBY -> "Tracking when in range, but not connected" + EventType.QIBLA_DIRECTION_PRAYER_TIME -> "Direction to Kibla" + EventType.DISPLAY_PINS_MAP_TILE -> "Displaying Pins on Map" + else -> "Unknown Event" + }, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 4.dp) ) diff --git a/build.gradle b/build.gradle index acd0942..ee05103 100644 --- a/build.gradle +++ b/build.gradle @@ -3,4 +3,5 @@ plugins { id 'com.android.application' version '8.5.2' apply false id 'com.android.library' version '8.5.2' apply false id 'org.jetbrains.kotlin.android' version '1.9.25' apply false + id 'com.google.dagger.hilt.android' version '2.52' apply false }