Skip to content

Commit

Permalink
Features/readtimestamps: Read timestamps from FireStore; Pause Upload…
Browse files Browse the repository at this point in the history
…ing to FireStore and Hide/Unhide Timestamps (#39)

# Read timestamps from FireStore; Pause Uploading to FireStore and
Hide/Unhide Timestamps

## ♻️ Current situation & Problem
Relevant issues:[ UI trigger for changing a flag field to true for data
that the user wants redacted
](#28)

Currently, the DeleteDataView that allows users to review and “hide”
their data uses a hard-coded array of timestamps, so it does not access
nor read in timestamps for the specified data type from Firestore.
Second, users cannot “unhide” their data after choosing to hide it.
Third, DeleteDataView currently does not support hiding data via a
custom date range.

To address the first issue, we created a function to fetch the10 most
recent timestamps from Firestore. In DeleteDataView, we display this
list under the “Hide by Timestamp” section.

Second, we changed all functionality from “deleting” data to “hiding”
data to better represent what is actually happening to user data on the
backend. We modified addDeleteFlag to switchHideFlag so that users can
“unhide” their data and have this status change reflected in Firestore’s
hideFlag field. We also modified the UI so that an open/crossed-out eye
is shown signaling which timestamps are hidden/unhidden.

Third, we created the “Hide by Custom Range” section in DeleteDataView
so users can select a date range to delete data via the date pickers. We
created the function fetchCustomRangeTimeStamps() to get a list of all
timestamps in the specified date range. Since we want all data in this
range to be hidden regardless of the previous hideFlag values, we
modified switcHed Flag to take in a third parameter (read below).

Lastly, we made modifications to communicate the enable/disable status
of sample category uploads between the DeleteDataView, ManageDataView,
and PrivacyModule. Saving the toggle value allowed users to pause or
unpause uploads of that data type to firestor (see
PrismaStandard+HealthKit).


## ⚙️ Release Notes 
### PrismaStandard+Healthkit:
- Before uploading a HealthKit datapoint to Firestore, check the toggle
status in the identifierInfo dictionary in PrivacyModule . If the toggle
is disabled, the HealthKit datapoint should not be uploaded to
Firestore.
- switchHideFlag(): Modified switchHideFlag logic so users can
hide/unhide data to update hideFlag in Firestore accordingly. Also
modified to take in an extra parameter “alwaysHide” bool. If the user
hides by custom range, “alwaysHide” is set to true, meaning the
hideFlags are always set to true regardless of original value.
Otherwise, the hideFlag is toggled (false set to true and true set to
false).
- fetchTop10RecentTimeStamps(): Create function which fetches the top 10
most recent timestamps for a given data type (e.g, step count) and
returns the populated array.
- fetchCustomRangeTimeStamps(): Create function which fetches all
timestamps in given date range by start date and end date pickers in
“hide by custom range” and returns the populated array.

### PrivacyModule:
- identifierInfo dictionary: maps the identifier string (e.g.
“stepcount”, “heartrate”) to information needed for the UI and the
hideFlag functionality (e.g. enabledBool status).
- sortedSampleIdentifiers list: using the sampleTypes list in
PrismaDelegate, send a list of the string identifiers for each type with
prefixes trimmed – ensures that only the sample types allowed by the
Prisma app will be displayed to the user in ManageDataView. Refactoring
this allowed us to create a publisher that signaled views to refresh
upon changes to the identifierInfo dictionary.
- identifierInfoSubject Combine publisher that sent signals to views
subscribed – views will refresh upon changes to the sample type toggles
(e.g. disabling the upload of step count data points)
- updateAndSignalOnChange() function that updates the identifierInfo
dictionary with the new toggle boolean and sends out a signal to
subscribing views.

### ManageDataView: 
- Updated to use identifierInfo  dictionary and sampleTypes list 
- Subscribed to the identifierInfoSubject Combine publisher; upon
receiving a signal from the publisher, the view refreshes its
“Disabled”/”Enabled” status

### DeleteDataView: 
- In “Hide by Custom Range” section, created date pickers for start and
end dates so users can select a date range to hide all data in between
dates. Pass the start and end date into fetchCustomRangeTimeStamps() to
fetch all timestamps in this range, then call switchHideFlag() with
alwaysHide = true to set hideFlags to true.
- In “Hide by Timestamps” section, display the timestamp array populated
by fetchTop10RecentTimeStamps(). Upon tapping the eye icon, hidden
status switches as reflected in the change in icon and
graying/un-graying out. Then, call switchHiddenInBackend to hide
timestamp in Firestore.
- Updated to use identifierInfo  dictionary
- When a new value is set for the toggle, it calls
updateAndSignalOnChange() to update the dictionary and send out signals
to subscribing views.

### Next Steps: 
Write unit tests for implemented features. 

## 📚 Documentation
N/A


## ✅ Testing

- In featureFlags.swift, switch set useFirebaseEmulator to false. 
- Replace the appropriate GoogleService-Info.plist 
- Erase all content and settings from the simulator device. 
- Run the app and login using personal account and password
(corresponding to account on FireStore)

## Roles
*Evelyn:*
- Create switchHideFlag() function in PrismaStandard+Healthkit to switch
hideFlag from false to true and vice versa in Firestore to reflect
hide/unhide timestamp changes by user in DeleteDataView.
- Create fetchTop10RecentTimeStamps() function which fetches the top 10
most recent timestamps for a given data type (e.g, step count), stores
the timestamps into an array, and returns the array (which will be
called in DeleteDataView).
- Create fetchCustomRangeTimeStamps() function which fetches all
timestamps in given date range by date picker in “Hide by custom range”
- Modify switchHideFlag() function to take in an additional parameter
alwaysHide bool. alwaysHide is set to true if the user hides by custom
range, otherwise it’s set to false.
- Create UI for hide by custom range using date pickers (calendar view)
in DeleteDataView to allow users to hide data in a specified date range
- Create UI for displaying 10 recent timestamps for the data category
item, showing each timestamp’s Firestore hideFlag status via an
open/crossed-out eye.
- Create switchHiddenInBackend() function inside DeleteDataView that
calls switchHideFlag in PrismaStandard+HealthKit to switch hideFlag
status

*Caroline:*
- Refactored data communication between the PrivacyModule,
ManageDataView, and DeleteDataView to reference and use only the
HKSampleTypes from the PrismaDelegate by creating
sortedSampleIdentifiers list.
SortedSampleIdentifiers list alphabetized the sample types to display
for the user. This list kept all UI information within the dictionary in
PrivacyModule and passed on as few parameters as possible. The
refactoring of this was necessary to ensure that the app’s HKSampleTypes
were the only ones shown to the user (allowed the app to be flexible and
modular to future app-wide modifications). The new structure also
allowed for the publisher addition mentioned in the next point.
- Created the Combine publisher in the PrivacyModule to signal all
subscribing views to refresh their pages with new identifierInfo
dictionary values.
- Debugging: An issue we ran into during this PR was how different
devices and different types of samples had different
effectiveDateTime/effectivePeriod fields. With Matt’s help, the
timestamps were intended to be sorted by “time.datetime.start” but in
swift, the “datetime.start” field was not recognizable so we resorted to
using the “issued” time.

*Dhruv:*
- writeToFirestore(): took Evelyn and Caroline’s code for writing to
firestore and put it into its own function for sake of modularity
- add(): included functionality in the PrismaStandard to not write to
Firestore if the HKSample has an off toggle
- PrivacyModule(): refactored a majority of the PrivacyModule class to
include initialization with a list of HKSampleTypes, and used this to
map each corresponding identifier to a bool for representation in
ManageDataView and DeleteDataView


## 📝 Code of Conduct & Contributing Guidelines 
By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md):
- I agree to follow the [Code of
Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md).

<img
src="https://github.com/CS342/2024-Prisma/assets/89322465/13335700-9070-4de1-8a33-0afc9960a4c7"
width="100" height="225">
<img
src="https://github.com/CS342/2024-Prisma/assets/89322465/de60dd50-25c8-4188-9b75-f12064fbac50"
width="100" height="225">
<img
src="https://github.com/CS342/2024-Prisma/assets/89322465/2d18cc38-98d0-4919-a067-e04e7f69df5c"
width="100" height="225">

---------

Co-authored-by: Evelyn <[email protected]>
Co-authored-by: dhruvna1k <[email protected]>
  • Loading branch information
3 people authored Mar 11, 2024
1 parent c3e1ceb commit 257bbcd
Show file tree
Hide file tree
Showing 10 changed files with 618 additions and 176 deletions.
4 changes: 0 additions & 4 deletions Prisma.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; };
A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */; };
AC69903E2B6C5A2F00D92970 /* PrivacyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */; };
AC6990402B6C627100D92970 /* ToggleTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903F2B6C627100D92970 /* ToggleTestView.swift */; };
D8027E912B90655700BB9466 /* ManageDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8027E902B90655700BB9466 /* ManageDataView.swift */; };
D8F136C52B85CEED000BA7AE /* DeleteDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */; };
E4C766262B72D50500C1DEDA /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C766252B72D50500C1DEDA /* WebView.swift */; };
Expand Down Expand Up @@ -158,7 +157,6 @@
A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = "<group>"; };
A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = "<group>"; };
AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyModule.swift; sourceTree = "<group>"; };
AC69903F2B6C627100D92970 /* ToggleTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleTestView.swift; sourceTree = "<group>"; };
D8027E902B90655700BB9466 /* ManageDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageDataView.swift; sourceTree = "<group>"; };
D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteDataView.swift; sourceTree = "<group>"; };
E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -418,7 +416,6 @@
isa = PBXGroup;
children = (
AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */,
AC69903F2B6C627100D92970 /* ToggleTestView.swift */,
D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */,
D8027E902B90655700BB9466 /* ManageDataView.swift */,
);
Expand Down Expand Up @@ -706,7 +703,6 @@
A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */,
D8027E912B90655700BB9466 /* ManageDataView.swift in Sources */,
F8AF6FB42B5F6EDC0011C32D /* PrismaModule.swift in Sources */,
AC6990402B6C627100D92970 /* ToggleTestView.swift in Sources */,
AC69903E2B6C5A2F00D92970 /* PrivacyModule.swift in Sources */,
653A2551283387FE005D4D48 /* Prisma.swift in Sources */,
2FE5DC3629EDD7CA004B9AB4 /* HealthKitPermissions.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"originHash" : "2744d3b6cb9385cb089a3b7f85f47cd50be0044a6d049ff1f93f530ab4329df2",
"pins" : [
{
"identity" : "abseil-cpp-binary",
Expand Down Expand Up @@ -316,5 +317,5 @@
}
}
],
"version" : 2
"version" : 3
}
134 changes: 96 additions & 38 deletions Prisma/PrivacyControls/DeleteDataView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,59 +27,117 @@ struct DeleteDataView: View {
var categoryIdentifier: String

// NEXT STEPS: timeArrayStatic will be replaced by timestampsArray which is read in from firestore using the categoryIdentifier and getPath
@State private var timeArrayStatic = ["2023-11-14T20:39:44.467", "2023-11-14T20:41:00.000", "2023-11-14T20:42:00.000"]
@State private var timeArrayStatic: [String] = []
// var timeArray = getLastTimestamps(quantityType: "stepcount")
@State private var crossedOutTimestamps: [String: Bool] = [:]
@State private var customHideStartDate = Date()
@State private var customHideEndDate = Date()
@State private var customRangeTimestamps: [String] = []

// state variable for the category toggle
@State private var isCategoryToggleOn = false

var body: some View {
// create a list of all the time stamps for this category
// get rid of spacing once we insert custom time range
VStack(spacing: -400) {
Form {
Section(header: Text("Allow to Read")) {
Toggle(self.privacyModule.identifierUIString[self.categoryIdentifier] ?? "Cannot Find Data Type", isOn: Binding<Bool>(
get: {
// Return the current value or a default value if the key does not exist
self.privacyModule.togglesMap[self.categoryIdentifier] ?? false
},
set: { newValue in
// Update the dictionary with the new value
self.privacyModule.togglesMap[self.categoryIdentifier] = newValue
}
))
}
Form {
descriptionSection
toggleSection
hideByCustomRangeSection
hideByTimeSection
}
.navigationTitle(privacyModule.identifierInfo[categoryIdentifier]?.uiString ?? "Identifier Title Not Found")
.onAppear {
Task {
timeArrayStatic = await standard.fetchTop10RecentTimeStamps(selectedTypeIdentifier: categoryIdentifier)
}
NavigationView {
// Toggle corresponding to the proper data to exclude all data of this type
List {
Section(header: Text("Delete by time")) {
ForEach(timeArrayStatic, id: \.self) { timestamp in
Text(timestamp)
}
// on delete, remove it on the UI and set flag in firebase
.onDelete { indices in
let timestampsToDelete = indices.map { timeArrayStatic[$0] }
deleteInBackend(identifier: categoryIdentifier, timestamps: timestampsToDelete)
timeArrayStatic.remove(atOffsets: indices)
}
}
}

var descriptionSection: some View {
Section(header: Text("About")) {
Text(privacyModule.identifierInfo[categoryIdentifier]?.description ?? "Missing Description.")
}
}

var toggleSection: some View {
Section(header: Text("Allow Data Upload")) {
Toggle(privacyModule.identifierInfo[categoryIdentifier]?.uiString ?? "Missing UI Type String ", isOn: Binding<Bool>(
get: {
// get the current enable status for the toggle
// default to a disabled toggle if the value is missing
privacyModule.identifierInfo[categoryIdentifier]?.enabledBool ?? false
},
set: { newValue in
// Update dict with new toggle status, signal to other views about dict change
privacyModule.updateAndSignalOnChange(identifierString: categoryIdentifier, newToggleVal: newValue)
}
))
}
}

var hideByCustomRangeSection: some View {
Section(header: Text("Hide Data by Custom Range")) {
VStack {
DatePicker("Start date", selection: $customHideStartDate, displayedComponents: .date)
DatePicker("End date", selection: $customHideEndDate, displayedComponents: .date)

Divider()

Button("Hide") {
let startDateString = formatDate(customHideStartDate)
let endDateString = formatDate(customHideEndDate)
Task {
customRangeTimestamps = await standard.fetchCustomRangeTimeStamps(
selectedTypeIdentifier: categoryIdentifier,
startDate: startDateString,
endDate: endDateString
)
}
switchHiddenInBackend(identifier: categoryIdentifier, timestamps: customRangeTimestamps, alwaysHide: true)
}
.padding(.top, -40)
.navigationBarItems(trailing: EditButton())
// .frame(maxWidth: .infinity) // Make the button take full width
}
}
.navigationTitle(privacyModule.identifierUIString[categoryIdentifier] ?? "Identifier Title Not Found")
}

func deleteInBackend(identifier: String, timestamps: [String]) {
var hideByTimeSection: some View {
Section(header: Text("Hide by Timestamps")) {
timeStampsDisplay
}
}

var timeStampsDisplay: some View {
ForEach(timeArrayStatic, id: \.self) { timestamp in
HStack {
Image(systemName: crossedOutTimestamps[timestamp, default: false] ? "eye.slash" : "eye")
.accessibilityLabel(crossedOutTimestamps[timestamp, default: false] ? "Hide Timestamp" : "Show Timestamp")
.onTapGesture {
switchHiddenInBackend(identifier: categoryIdentifier, timestamps: [timestamp], alwaysHide: false)
crossedOutTimestamps[timestamp]?.toggle() ?? (crossedOutTimestamps[timestamp] = true)
}
Text(timestamp)
}
.foregroundColor(crossedOutTimestamps[timestamp, default: false] ? .gray : .black)
.opacity(crossedOutTimestamps[timestamp, default: false] ? 0.5 : 1.0)
}
}

func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter.string(from: date)
}

func switchHiddenInBackend(identifier: String, timestamps: [String], alwaysHide: Bool) {
for timestamp in timestamps {
Task {
await standard.addDeleteFlag(selectedTypeIdentifier: identifier, timestamp: timestamp)
await standard.switchHideFlag(selectedTypeIdentifier: identifier, timestamp: timestamp, alwaysHide: alwaysHide)
}
}
}
}


#Preview {
DeleteDataView(categoryIdentifier: "Example Preview: DeleteDataView")
struct DeleteDataView_Previews: PreviewProvider {
static var previews: some View {
DeleteDataView(categoryIdentifier: "Example Preview: DeleteDataView")
}
}
41 changes: 26 additions & 15 deletions Prisma/PrivacyControls/ManageDataView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,40 @@
import SwiftUI

struct ManageDataView: View {
@Environment(PrivacyModule.self) private var privacyModule
@EnvironmentObject var privacyModule: PrivacyModule

var body: some View {
NavigationView {
List(privacyModule.dataCategoryItems, id: \.name) { item in
NavigationLink(destination: DeleteDataView(categoryIdentifier: item.name)) {
HStack(alignment: .center, spacing: 10) {
Image(systemName: item.iconName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35, height: 35)
.accessibility(label: Text("accessibility text temp"))
VStack(alignment: .leading, spacing: 4) {
Text(privacyModule.identifierUIString[item.name] ?? "Identifier UI String Not Found")
.font(.headline)
Text(item.enabledStatus)
.font(.subheadline)
.foregroundColor(.gray)
List {
ForEach(privacyModule.sortedSampleIdentifiers, id: \.self) { sampleIdentifier in
NavigationLink(
destination: DeleteDataView(
categoryIdentifier: privacyModule.identifierInfo[sampleIdentifier]?.identifier ?? "missing identifier string"
)
) {
HStack(alignment: .center, spacing: 10) {
Image(systemName: privacyModule.identifierInfo[sampleIdentifier]?.iconName ?? "missing icon name")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35, height: 35)
.accessibility(label: Text("accessibility text temp"))
VStack(alignment: .leading, spacing: 4) {
Text(privacyModule.identifierInfo[sampleIdentifier]?.uiString ?? "missing ui identifier string")
.font(.headline)
// assume a default value of false if there is a nil value in enabledBool
Text((privacyModule.identifierInfo[sampleIdentifier]?.enabledBool ?? false) ? "Enabled" : "Disabled")
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
}
}
.navigationTitle("Manage Data")
}
.onReceive(privacyModule.identifierInfoPublisher) { _ in
self.privacyModule.objectWillChange.send()
}
}
}

Expand Down
Loading

0 comments on commit 257bbcd

Please sign in to comment.