diff --git a/.prettierrc.js b/.prettierrc.js index 5c4de1a4f6..355e7d1e4b 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,5 +1,5 @@ module.exports = { - bracketSpacing: false, + bracketSpacing: true, jsxBracketSameLine: true, singleQuote: true, trailingComma: 'all', diff --git a/App.js b/App.js index 1f5e6f53fe..4721698b1d 100644 --- a/App.js +++ b/App.js @@ -7,17 +7,15 @@ */ import React from 'react'; -import { - StatusBar -} from 'react-native'; - +import { StatusBar } from 'react-native'; +import { MenuProvider } from 'react-native-popup-menu'; import Entry from './app/Entry'; const App: () => React$Node = () => { return ( - <> - - + + + ); }; diff --git a/.github/CONTRIBUTING.md b/CONTRIBUTING.md similarity index 98% rename from .github/CONTRIBUTING.md rename to CONTRIBUTING.md index 176eb95478..0088e98773 100644 --- a/.github/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ prepare and submit a pull request. * You need a Github account. You can [create one](https://github.com/signup/free) for free. -* Submit an [Issue](https://github.com/tripleblind/mobileapp/issues) against +* Submit an [Issue](https://github.com/tripleblindmarket/private-kit/issues) against the repo to describe the idea or problem if there is not one yet. * Describe a bug by including steps to reproduce and earliest version you know is affected. diff --git a/README.md b/README.md index 4eae2fc422..f15fb130c6 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,14 @@ Private Kit’s trail generator logs your device’s location once every five mi **Downloads:** [Google Play](https://play.google.com/store/apps/details?id=edu.mit.privatekit) | [Apple Store](https://apps.apple.com/us/app/private-kit-prototype/id1501903733) -# Development +# Development Overview This is a React Native app version 61.5 +## Architecture + +Please refer to `docs/Private Kit Diagram.png` for a basic overview on the sequencing of generalized events and services that are utilized by Private Kit. + ## Developer Setup Refer to and run the dev_setup.sh for needed tools. @@ -50,20 +54,13 @@ or npx react-native run-ios --simulator="iPhone 8 Plus" ``` ----------------------------------------------------------------------------------- NOTE: In some cases, the abovementioned procedure leads to the error 'Failed to load bundle - Could not connect to development server'. In these cases, kill all other react-native processes and try it again. ----------------------------------------------------------------------------------- ## Contributing -Read the [contribution guidelines](./.github/CONTRIBUTING.md). - -Join the WhatsApp group - https://chat.whatsapp.com/HXonYGVeAwQIKxO0HYlxYL +Read the [contribution guidelines](CONTRIBUTING.md). -## Tested On +WhatsApp: https://chat.whatsapp.com/HXonYGVeAwQIKxO0HYlxYL +Slack: https://safepathsprivatekit.slack.com/ -| Device | Version | -| ------------- | ------------- | -| Android Pixel | API 28 | -| Android Pixel | API 29 | diff --git a/android/app/build.gradle b/android/app/build.gradle index eaea358522..6093cb2bec 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -131,8 +131,9 @@ android { applicationId "edu.mit.privatekit" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 8 - versionName "0.5.3" + multiDexEnabled true + versionCode 11 + versionName "0.5.4" } splits { abi { @@ -205,8 +206,38 @@ dependencies { } else { implementation jscFlavor } + + implementation(project(':react-native-maps')){ + exclude group: 'com.google.android.gms', module: 'play-services-base' + exclude group: 'com.google.android.gms', module: 'play-services-maps' + } + implementation 'com.google.android.gms:play-services-base:10.0.1' + implementation 'com.google.android.gms:play-services-maps:10.0.1' } + // Just to fix the app start crash error + allprojects { + repositories { + //start here + configurations.all { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + def requested = details.requested + if (requested.group == 'com.google.android.gms') { + details.useVersion '12.0.1' + } + if (requested.group == 'com.google.firebase') { + details.useVersion '12.0.1' + } + } + } + //end + jcenter() + maven { + url "https://maven.google.com" + } + } + } + // Run this once to be able to run the application with BUCK // puts all compile dependencies into folder libs for BUCK to use task copyDownloadableDepsToLibs(type: Copy) { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1316a09fd4..ccb7761d68 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ android:roundIcon="@mipmap/ic_launcher" android:allowBackup="false" android:theme="@style/AppTheme"> + + { - console.log(isParticipating); - this.setState({ - initialRouteName:isParticipating - }) + console.log(isParticipating); + this.setState({ + initialRouteName: isParticipating, + }); }) - .catch(error => console.log(error)) - } + .catch(error => console.log(error)); + } - render() { - return ( - - - + render() { + return ( + + + {this.state.initialRouteName === 'true' ? ( - ):( + name="InitialScreen" + component={LocationTracking} + options={{ headerShown: false }} + /> + ) : ( + name="InitialScreen" + component={Slider} + options={{ headerShown: false }} + /> )} - - - + options={{ headerShown: false }} + /> + options={{ headerShown: false }} + /> + + - - - ) - } + + + ); + } } export default Entry; diff --git a/app/assets/images/kebabIcon.png b/app/assets/images/kebabIcon.png new file mode 100644 index 0000000000..8b3d663aa5 Binary files /dev/null and b/app/assets/images/kebabIcon.png differ diff --git a/app/components/Button.js b/app/components/Button.js index 6835648f2c..137ee48a45 100644 --- a/app/components/Button.js +++ b/app/components/Button.js @@ -1,6 +1,6 @@ -import * as React from "react"; -import { StyleSheet, Text, TouchableOpacity } from "react-native"; -import colors from "../constants/colors"; +import * as React from 'react'; +import { StyleSheet, Text, TouchableOpacity } from 'react-native'; +import colors from '../constants/colors'; interface Props { label: string; @@ -12,7 +12,12 @@ class Button extends React.Component { render() { const { title, onPress, bgColor } = this.props; return ( - + {title} ); @@ -21,21 +26,21 @@ class Button extends React.Component { const styles = StyleSheet.create({ container: { - width: "100%", - alignItems: "center", - justifyContent: "center", + width: '100%', + alignItems: 'center', + justifyContent: 'center', paddingVertical: 12, borderRadius: 4, borderWidth: StyleSheet.hairlineWidth, - borderColor: "rgba(255,255,255,0.7)" + borderColor: 'rgba(255,255,255,0.7)', }, text: { color: colors.WHITE, - textAlign: "center", + textAlign: 'center', height: 28, fontSize: 20, fontWeight: '600', - } + }, }); -export default Button; \ No newline at end of file +export default Button; diff --git a/app/constants/colors.js b/app/constants/colors.js index 292c4fdb88..0e54e2230d 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -1,19 +1,19 @@ const colors = { - BLACK: "#000", - WHITE: "#FFF", - DODGER_BLUE: "#428AF8", - SILVER: "#BEBEBE", - TORCH_RED: "#F8262F", - MISCHKA: "#E5E4E6", + BLACK: '#000', + WHITE: '#FFF', + DODGER_BLUE: '#428AF8', + SILVER: '#BEBEBE', + TORCH_RED: '#F8262F', + MISCHKA: '#E5E4E6', APP_BACKGROUND: '#FFF8ED', - PRIMARY_TEXT: "#000", - GREEN: "#32A852", - VIOLET: "#6C3794", + PRIMARY_TEXT: '#000', + GREEN: '#32A852', + VIOLET: '#6C3794', - REG_BUTTON: "#428AF8", - POS_BUTTON: "#32A852", - NEG_BUTTON: "#F8262F", - SENS_BUTTON: "#6C3794" + REG_BUTTON: '#428AF8', + POS_BUTTON: '#32A852', + NEG_BUTTON: '#F8262F', + SENS_BUTTON: '#6C3794', }; export default colors; diff --git a/app/encryption/intersection.py b/app/encryption/intersection.py new file mode 100644 index 0000000000..9869d45f71 --- /dev/null +++ b/app/encryption/intersection.py @@ -0,0 +1,135 @@ +import numpy as np +import hashlib +import random + +# ########################################################################## +# Example of an encrypted system in operation. This works with a few +# assumptions that can be adjusted: +# * Getting within approximately 70' is close enough to note +# * "Infection" sticks around for 2 hours +# +# Questions can be directed to TripleBlind, Inc. This code and algorithm +# is donated to the Private Kit project. + + +# ########################################################################## +# InfectedUser + +class InfectedUser: + def __init__(self): + self.salt = str(random.randint(0, 2 ** 100)).encode("utf-8") + + def infected_helper_generation(self, location, thresholds): + distance_threshold = thresholds[0] + time_threshold = int(thresholds[1] / 2) + lat = int(location[0] * 10 ** 6) + long = int(location[1] * 10 ** 6) + time_ = int( + location[2] + time_threshold / 2 + ) # an origin for time is needed let's say the day the app is released + template = [lat, long, time_] + random_x = random.randint( + int((-90 * 10 ** 6) / (2 * distance_threshold)), + int((90 * 10 ** 6) / (2 * distance_threshold)), + ) + random_y = random.randint( + int((-180 * 10 ** 6) / (2 * distance_threshold)), + int((180 * 10 ** 6) / (2 * distance_threshold)), + ) + random_time = random.randint(0, 2 ** 50) + lattice_point_x = random_x * 2 * distance_threshold + lattice_point_y = random_y * 2 * distance_threshold + lattice_point_z = random_time * 2 * time_threshold + lattice_point = np.array([lattice_point_x, lattice_point_y, lattice_point_z]) + translation_vector = lattice_point - template + hash_complexity = 1000000 + dk = hashlib.pbkdf2_hmac( + "sha256", str(lattice_point).encode("utf-8"), self.salt, hash_complexity + ) + + return translation_vector, dk.hex() + +# ######################################################################### + +def user_hash_generation(query, translation_vector, salt, thresholds): + lat = int(query[0] * 10 ** 6) + long = int(query[1] * 10 ** 6) + time_ = int(query[2]) + distance_threshold = thresholds[0] + time_threshold = int(thresholds[1] / 2) + query = np.array([lat, long, time_]) + translated_query = query + translation_vector + + quantized_query = ( + 2 + * distance_threshold + * np.ceil( + (translated_query[0:2] - distance_threshold) / (2 * distance_threshold) + ).astype(np.int64) + ) + quantized_time = ( + 2 + * time_threshold + * np.ceil((translated_query[2] - time_threshold) / (2 * time_threshold)).astype( + np.int64 + ) + ) + quantized_out = np.array([quantized_query[0], quantized_query[1], quantized_time]) + encoded = str(quantized_out).encode("utf-8") + hash_complexity = 1000000 + dk = hashlib.pbkdf2_hmac("sha256", encoded, salt, hash_complexity) + return dk.hex() + +# +# The infected user do the following +# * Store a set of points in GPS lat/lon coordinate system +# * Generate the unique hash and helper data + + +user1_locations = np.array( + [[41.403380, 39.289342, 32], [2.192491, 145.293971, 55]] +) # [lat,long,time] +inf_user = InfectedUser() +thresholds = [300, 2] # .000300 is approximately 70 feet #TODO: More accurate threshold +# 2 hours threshold + +user1_helper_data = [] +for i in range(user1_locations.shape[0]): + user1_helper_data.append( + inf_user.infected_helper_generation(user1_locations[i], thresholds) + ) + + +print(user1_helper_data[0][1], "infected point hash") +""" +The hash of the infected point is stored at the server but the other helper data translation vector, salt +is sent to all users + + +""" + +translation_vector = user1_helper_data[0][0] +salt = inf_user.salt +current_location1 = np.array([41.403380, 39.289342, 32]) # exact match +current_location2 = np.array([41.403280, 39.289142, 33]) # within threshold ( +current_location3 = np.array([41.403280, 39.289142, 31]) # before the infection +current_location4 = np.array([41.401380, 39.289342, 31]) # safe area +print( + user_hash_generation(current_location1, translation_vector, salt, thresholds), + "This point is close to an infected point within 2 hours", +) +print( + user_hash_generation(current_location2, translation_vector, salt, thresholds), + "This point is close to an infected point within 2 hours", +) +print( + user_hash_generation(current_location3, translation_vector, salt, thresholds), + "This point is safe", +) +print( + user_hash_generation(current_location4, translation_vector, salt, thresholds), + "This point is safe", +) + + +"""The Hash is sent to the server and server perform the matching """ diff --git a/app/helpers/General.js b/app/helpers/General.js index 6f0805edd4..bb874bcf0f 100644 --- a/app/helpers/General.js +++ b/app/helpers/General.js @@ -9,18 +9,18 @@ import _ from 'lodash'; * @param {boolean} isString */ export async function GetStoreData(key, isString = true) { - try { - let data = await AsyncStorage.getItem(key); + try { + let data = await AsyncStorage.getItem(key); - if (isString) { - return data; - } + if (isString) { + return data; + } - return JSON.parse(data); - } catch (error) { - console.log(error.message); - } - return false; + return JSON.parse(data); + } catch (error) { + console.log(error.message); + } + return false; } /** @@ -30,16 +30,16 @@ export async function GetStoreData(key, isString = true) { * @param {string} key * @param {object} item */ -export async function SetStoreData (key, item) { - try { - //we want to wait for the Promise returned by AsyncStorage.setItem() - //to be resolved to the actual value before returning the value - if (typeof item !== 'string') { - item = JSON.stringify(item); - } +export async function SetStoreData(key, item) { + try { + //we want to wait for the Promise returned by AsyncStorage.setItem() + //to be resolved to the actual value before returning the value + if (typeof item !== 'string') { + item = JSON.stringify(item); + } - return await AsyncStorage.setItem(key, item); - } catch (error) { - console.log(error.message); - } -} \ No newline at end of file + return await AsyncStorage.setItem(key, item); + } catch (error) { + console.log(error.message); + } +} diff --git a/app/helpers/GoogleData.js b/app/helpers/GoogleData.js index ebe8f37569..dfffb261da 100644 --- a/app/helpers/GoogleData.js +++ b/app/helpers/GoogleData.js @@ -1,65 +1,66 @@ /** - * Import a Google JSon into the Database. + * Import a Google JSon into the Database. */ import { GetStoreData, SetStoreData } from '../helpers/General'; function BuildLocalFormat(placeVisit) { - return loc = { - latitude: (placeVisit.location.latitudeE7 * (10 ** -7)), - longitude: (placeVisit.location.longitudeE7 * (10 ** -7)), - time: placeVisit.duration.startTimestampMs - }; + return (loc = { + latitude: placeVisit.location.latitudeE7 * 10 ** -7, + longitude: placeVisit.location.longitudeE7 * 10 ** -7, + time: placeVisit.duration.startTimestampMs, + }); } function LocationExists(localDataJSON, loc) { - var wasImportedBefore = false; + var wasImportedBefore = false; - for (var index = 0; index < localDataJSON.length; ++index) { - var storedLoc = localDataJSON[index]; - if (storedLoc.latitude == loc.latitude && - storedLoc.longitude == loc.longitude && - storedLoc.time == loc.time) { - wasImportedBefore = true; - break; - } + for (var index = 0; index < localDataJSON.length; ++index) { + var storedLoc = localDataJSON[index]; + if ( + storedLoc.latitude == loc.latitude && + storedLoc.longitude == loc.longitude && + storedLoc.time == loc.time + ) { + wasImportedBefore = true; + break; } + } - return wasImportedBefore; + return wasImportedBefore; } function InsertIfNew(localDataJSON, loc) { - if (!LocationExists(localDataJSON, loc)) { - console.log("Importing", loc); - localDataJSON.push(loc); - } else { - console.log("Existing", loc, localDataJSON.indexOf(loc)); - } + if (!LocationExists(localDataJSON, loc)) { + console.log('Importing', loc); + localDataJSON.push(loc); + } else { + console.log('Existing', loc, localDataJSON.indexOf(loc)); + } } function Merge(localDataJSON, googleDataJSON) { - googleDataJSON.timelineObjects.map(function (data, index) { - // Only import visited places, not paths for now - if (data.placeVisit) { - var loc = BuildLocalFormat(data.placeVisit); - InsertIfNew(localDataJSON, loc); - } - }); + googleDataJSON.timelineObjects.map(function(data, index) { + // Only import visited places, not paths for now + if (data.placeVisit) { + var loc = BuildLocalFormat(data.placeVisit); + InsertIfNew(localDataJSON, loc); + } + }); } export async function MergeJSONWithLocalData(googleDataJSON) { - GetStoreData('LOCATION_DATA') - .then(locationArray => { - var locationData; + GetStoreData('LOCATION_DATA').then(locationArray => { + var locationData; - if (locationArray !== null) { - locationData = JSON.parse(locationArray); - } else { - locationData = []; - } + if (locationArray !== null) { + locationData = JSON.parse(locationArray); + } else { + locationData = []; + } - Merge(locationData, googleDataJSON); + Merge(locationData, googleDataJSON); - console.log("Saving on array"); - SetStoreData('LOCATION_DATA', locationData); - }); -} \ No newline at end of file + console.log('Saving on array'); + SetStoreData('LOCATION_DATA', locationData); + }); +} diff --git a/app/helpers/GoogleTakeOutAutoImport.js b/app/helpers/GoogleTakeOutAutoImport.js index 276e524cfc..f7c6b3ca50 100644 --- a/app/helpers/GoogleTakeOutAutoImport.js +++ b/app/helpers/GoogleTakeOutAutoImport.js @@ -1,75 +1,99 @@ /** * Checks the download folder, unzips and imports all data from Google TakeOut */ -import { zip, unzip, unzipAssets, subscribe } from 'react-native-zip-archive' -import {MergeJSONWithLocalData} from '../helpers/GoogleData'; +import { zip, unzip, unzipAssets, subscribe } from 'react-native-zip-archive'; +import { MergeJSONWithLocalData } from '../helpers/GoogleData'; // require the module var RNFS = require('react-native-fs'); -// unzipping progress component. +// unzipping progress component. var progress; -// Google Takout File Format. +// Google Takout File Format. var takeoutZip = /^takeout[\w,\s-]+\.zip$/gm; -// Gets Path of the location file for the current month. +// Gets Path of the location file for the current month. function GetFileName() { - let monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + let monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; - var year = new Date().getFullYear(); - var month = monthNames[new Date().getMonth()].toUpperCase(); - return RNFS.DownloadDirectoryPath + "/Takeout/Location History/Semantic Location History/"+year+"/"+year+"_MARCH.json"; + var year = new Date().getFullYear(); + var month = monthNames[new Date().getMonth()].toUpperCase(); + return ( + RNFS.DownloadDirectoryPath + + '/Takeout/Location History/Semantic Location History/' + + year + + '/' + + year + + '_MARCH.json' + ); } export async function SearchAndImport(googleLocationJSON) { - console.log('Auto-import start'); + console.log('Auto-import start'); - // UnZip Progress Bar Log. - progress = subscribe(({ progress, filePath }) => { + // UnZip Progress Bar Log. + progress = subscribe(({ progress, filePath }) => { if (Math.trunc(progress * 100) % 10 === 0) - console.log('Unzipping', Math.trunc(progress * 100), '%'); - }); + console.log('Unzipping', Math.trunc(progress * 100), '%'); + }); + + // TODO: RNFS.DownloadDirectoryPath is not defined on iOS. + // Find out how to access Downloads folder. + if (!RNFS.DownloadDirectoryPath) { + return; + } - // TODO: RNFS.DownloadDirectoryPath is not defined on iOS. - // Find out how to access Downloads folder. - if (!RNFS.DownloadDirectoryPath) { - return; - } + RNFS.readDir(RNFS.DownloadDirectoryPath) + .then(result => { + console.log('Checking Downloads Folder'); - RNFS.readDir(RNFS.DownloadDirectoryPath) - .then((result) => { - console.log('Checking Downloads Folder'); - - // Looking for takeout*.zip files and unzipping them. - result.map(function(file, index){ - if (takeoutZip.test(file.name)) { - console.log(`Found Google Takeout {file.name} at {file.path}`, file.name); + // Looking for takeout*.zip files and unzipping them. + result.map(function(file, index) { + if (takeoutZip.test(file.name)) { + console.log( + `Found Google Takeout {file.name} at {file.path}`, + file.name, + ); - unzip(file.path, RNFS.DownloadDirectoryPath) - .then((path) => { - console.log(`Unzip Completed for ${path} and ${file.path}`); - - RNFS.readFile(GetFileName()).then((result) => { - console.log("Opened file"); - - MergeJSONWithLocalData(JSON.parse(result)); - progress.remove(); + unzip(file.path, RNFS.DownloadDirectoryPath) + .then(path => { + console.log(`Unzip Completed for ${path} and ${file.path}`); - }).catch((err) => { - console.log(err.message, err.code); - progress.remove(); - }); - }) - .catch((error) => { - console.log(error); - progress.remove(); - }); - } + RNFS.readFile(GetFileName()) + .then(result => { + console.log('Opened file'); + + MergeJSONWithLocalData(JSON.parse(result)); + progress.remove(); + }) + .catch(err => { + console.log(err.message, err.code); + progress.remove(); + }); + }) + .catch(error => { + console.log(error); + progress.remove(); }); - }) - .catch((err) => { - console.log(err.message, err.code); - progress.remove(); - }); + } + }); + }) + .catch(err => { + console.log(err.message, err.code); + progress.remove(); + }); } diff --git a/app/helpers/convertPointsToString.js b/app/helpers/convertPointsToString.js index cdb0ab4fa6..97e653e9de 100644 --- a/app/helpers/convertPointsToString.js +++ b/app/helpers/convertPointsToString.js @@ -1,50 +1,51 @@ -export function convertPointsToString(count){ - - // For testing Manually override count - // count = 3000 +export function convertPointsToString(count) { + // For testing Manually override count + // count = 3000 - // Get minutes - let tot_mins = count * 5; + // Get minutes + let tot_mins = count * 5; - // Calculate days - var days = (tot_mins / 60/ 24); - var rdays = Math.floor(days); + // Calculate days + var days = tot_mins / 60 / 24; + var rdays = Math.floor(days); - // Calculate Hours - var hours = (days - rdays) * 24; - var rhours = Math.floor(hours); + // Calculate Hours + var hours = (days - rdays) * 24; + var rhours = Math.floor(hours); - // Calculate Minutes - var minutes = (hours - rhours) * 60; - var rminutes = Math.round(minutes); + // Calculate Minutes + var minutes = (hours - rhours) * 60; + var rminutes = Math.round(minutes); - if(rdays > 0){ - if(rdays > 1){ - if(rhours > 1){ - return rdays + " days, " + rhours + " hours and " + rminutes + " minutes."; - } - else{ - return rdays + " days, " + rhours + " hour and " + rminutes + " minutes."; - } - } - else{ - if(rhours > 1){ - return rdays + " day, " + rhours + " hours and " + rminutes + " minutes."; - } - else{ - return rdays + " day, " + rhours + " hour and " + rminutes + " minutes."; - } - } - } - else if(rhours > 0 ){ - if(rhours > 1){ - return rhours + " hours and " + rminutes + " minutes."; - } - else{ - return rhours + " hour and " + rminutes + " minutes."; - } - } - else{ - return rminutes + " minutes."; + if (rdays > 0) { + if (rdays > 1) { + if (rhours > 1) { + return ( + rdays + ' days, ' + rhours + ' hours and ' + rminutes + ' minutes.' + ); + } else { + return ( + rdays + ' days, ' + rhours + ' hour and ' + rminutes + ' minutes.' + ); + } + } else { + if (rhours > 1) { + return ( + rdays + ' day, ' + rhours + ' hours and ' + rminutes + ' minutes.' + ); + } else { + return ( + rdays + ' day, ' + rhours + ' hour and ' + rminutes + ' minutes.' + ); + } } - } \ No newline at end of file + } else if (rhours > 0) { + if (rhours > 1) { + return rhours + ' hours and ' + rminutes + ' minutes.'; + } else { + return rhours + ' hour and ' + rminutes + ' minutes.'; + } + } else { + return rminutes + ' minutes.'; + } +} diff --git a/app/helpers/customCircle.js b/app/helpers/customCircle.js new file mode 100644 index 0000000000..a2e7aeabd3 --- /dev/null +++ b/app/helpers/customCircle.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { Circle } from 'react-native-maps'; + +function CustomCircle({ onLayout, ...props }) { + const ref = React.useRef(); + + function onLayoutCircle() { + if (ref.current) { + ref.current.setNativeProps({ fillColor: props.fillColor }); + } + // call onLayout() from the props if you need it + } + + return ; +} + +export default CustomCircle; \ No newline at end of file diff --git a/app/locales/de/index.js b/app/locales/de/index.js index 77d79a8a10..c63e419c7e 100644 --- a/app/locales/de/index.js +++ b/app/locales/de/index.js @@ -1,11 +1,11 @@ import intro from './intro.json'; import locationTracking from './locationTracking.json'; -import importFile from './import.json' -import exportFile from './exportscreen.json' +import importFile from './import.json'; +import exportFile from './exportscreen.json'; export default { - ...intro, - ...locationTracking, - ...importFile, - ...exportFile + ...intro, + ...locationTracking, + ...importFile, + ...exportFile, }; diff --git a/app/locales/en/index.js b/app/locales/en/index.js index 77d79a8a10..deec7ca2a7 100644 --- a/app/locales/en/index.js +++ b/app/locales/en/index.js @@ -2,10 +2,14 @@ import intro from './intro.json'; import locationTracking from './locationTracking.json'; import importFile from './import.json' import exportFile from './exportscreen.json' +import licensesFile from './licensesscreen.json'; +import overlapFile from './overlap.json' export default { ...intro, ...locationTracking, ...importFile, - ...exportFile + ...exportFile, + ...overlapFile, + ...licensesFile, }; diff --git a/app/locales/en/intro.json b/app/locales/en/intro.json index 9e1f07ae35..0ed6879516 100644 --- a/app/locales/en/intro.json +++ b/app/locales/en/intro.json @@ -7,8 +7,8 @@ "intro2_title1":"Less than 100KB", "intro2_para1":"Private Kit’s trail generator logs your device’s location data in under 100KB of space – less space than a single picture.", "intro2_title2":"You are in charge", - "intro2_para2":"Data Never Leaves Your Device Without Your Consent", + "intro2_para2": "Data Never Leaves Your Device Without Your Consent", "intro3_title1":"The Future", - "intro3_para1":"The Next Step in Solving Today and Tomorrow’s Problems Enabling individuals to log their location trail offers new opportunities for researchers studying pandemic tracking, refugee migration, and community traffic analysis.", + "intro3_para1": "The Next Step in Solving Today's and Tomorrow’s Problems Enabling individuals to log their location trail offers new opportunities for researchers studying pandemic tracking, refugee migration, and community traffic analysis.", "intro3_para2":"Learn More http://privatekit.mit.edu/" -} \ No newline at end of file +} diff --git a/app/locales/en/licensesscreen.json b/app/locales/en/licensesscreen.json new file mode 100644 index 0000000000..9be7837196 --- /dev/null +++ b/app/locales/en/licensesscreen.json @@ -0,0 +1,3 @@ +{ + "license_placeholder": "This app is built under the MIT license." +} \ No newline at end of file diff --git a/app/locales/en/locationTracking.json b/app/locales/en/locationTracking.json index b2736182db..2933bec3c7 100644 --- a/app/locales/en/locationTracking.json +++ b/app/locales/en/locationTracking.json @@ -7,6 +7,7 @@ "export":"Export", "news":"news", "latest_news":"Latest News", - "url_info":"For more information visit the Private Kit homepage:", - "private_kit_url":"privatekit.mit.edu" + "url_info":"For more information visit the Private Kit hompage:", + "private_kit_url":"privatekit.mit.edu", + "overlap": "CHECK OVERLAP", } \ No newline at end of file diff --git a/app/locales/en/overlap.json b/app/locales/en/overlap.json new file mode 100644 index 0000000000..9942c4b33d --- /dev/null +++ b/app/locales/en/overlap.json @@ -0,0 +1,4 @@ +{ + "overlap_para_1":"This map shows where your private location trail overlaps with public data from a variety of sources, including official reports from WHO, Ministries of Health, and Chinese local, provincial, and national health authorities. If additional data are available from reliable online reports, they are included.", + "show_overlap": "SHOW ME TRACE OVERLAP", +} \ No newline at end of file diff --git a/app/locales/fr/index.js b/app/locales/fr/index.js index 77d79a8a10..c63e419c7e 100644 --- a/app/locales/fr/index.js +++ b/app/locales/fr/index.js @@ -1,11 +1,11 @@ import intro from './intro.json'; import locationTracking from './locationTracking.json'; -import importFile from './import.json' -import exportFile from './exportscreen.json' +import importFile from './import.json'; +import exportFile from './exportscreen.json'; export default { - ...intro, - ...locationTracking, - ...importFile, - ...exportFile + ...intro, + ...locationTracking, + ...importFile, + ...exportFile, }; diff --git a/app/locales/hi/index.js b/app/locales/hi/index.js index 77d79a8a10..c63e419c7e 100644 --- a/app/locales/hi/index.js +++ b/app/locales/hi/index.js @@ -1,11 +1,11 @@ import intro from './intro.json'; import locationTracking from './locationTracking.json'; -import importFile from './import.json' -import exportFile from './exportscreen.json' +import importFile from './import.json'; +import exportFile from './exportscreen.json'; export default { - ...intro, - ...locationTracking, - ...importFile, - ...exportFile + ...intro, + ...locationTracking, + ...importFile, + ...exportFile, }; diff --git a/app/locales/it/exportscreen.json b/app/locales/it/exportscreen.json index d834b2f95a..497c48e48a 100644 --- a/app/locales/it/exportscreen.json +++ b/app/locales/it/exportscreen.json @@ -1,6 +1,6 @@ { - "export_para_1":"You can share your location trail with anyone using the Share button below. Once you press the button it will ask you with whom and how you want to share it.", - "export_para_2":"Location is shared as a simple list of times and coordinates, no other identifying information.", - "share":"SHARE", - "data_hint":"Log has data of" -} \ No newline at end of file + "export_para_1":"Puoi condividere i tuoi movimenti con chiunque, premendo il tasto CONDIVIDI. Una volta premuto il tasto, l'applicazione ti chiederà con chi e come vuoi condivide le informazioni.", + "export_para_2":"La posizione è condivisa come una semplice lista di date e coordinate, senza alcuna informazione che possa permettere l'identificazione dell'utente.", + "share":"CONDIVIDI", + "data_hint":"Log contiene dati di" +} diff --git a/app/locales/it/import.json b/app/locales/it/import.json index 58e337de66..268dce5c65 100644 --- a/app/locales/it/import.json +++ b/app/locales/it/import.json @@ -1,5 +1,5 @@ { - "import_title":"Import Locations", - "import_step_1":"1. Login to your Google Account and Download your Location History", - "import_step_2":"2. After downloaded, open this screen again. The data will import automatically." -} \ No newline at end of file + "import_title":"Importa le Posizioni", + "import_step_1":"1. Fai login nel tuo account Google e scarica la storia dei tuoi movimenti.", + "import_step_2":"2. Una volta scaricati i movimenti, apri questa schermata. I dati saranno importati automaticamente." +} diff --git a/app/locales/it/index.js b/app/locales/it/index.js index 77d79a8a10..c63e419c7e 100644 --- a/app/locales/it/index.js +++ b/app/locales/it/index.js @@ -1,11 +1,11 @@ import intro from './intro.json'; import locationTracking from './locationTracking.json'; -import importFile from './import.json' -import exportFile from './exportscreen.json' +import importFile from './import.json'; +import exportFile from './exportscreen.json'; export default { - ...intro, - ...locationTracking, - ...importFile, - ...exportFile + ...intro, + ...locationTracking, + ...importFile, + ...exportFile, }; diff --git a/app/locales/it/intro.json b/app/locales/it/intro.json index 9e1f07ae35..6e5364a3de 100644 --- a/app/locales/it/intro.json +++ b/app/locales/it/intro.json @@ -1,14 +1,14 @@ { "private_kit":"Private Kit", - "intro1_para1":"Designed with data security and privacy protection at its heart, MIT Private Kit is the next generation of secure location logging.", - "next":"NEXT", - "back":"BACK", - "start":"START", - "intro2_title1":"Less than 100KB", - "intro2_para1":"Private Kit’s trail generator logs your device’s location data in under 100KB of space – less space than a single picture.", - "intro2_title2":"You are in charge", - "intro2_para2":"Data Never Leaves Your Device Without Your Consent", - "intro3_title1":"The Future", - "intro3_para1":"The Next Step in Solving Today and Tomorrow’s Problems Enabling individuals to log their location trail offers new opportunities for researchers studying pandemic tracking, refugee migration, and community traffic analysis.", - "intro3_para2":"Learn More http://privatekit.mit.edu/" -} \ No newline at end of file + "intro1_para1":"Progettato tenendo a cuore la sicurezza dei dati e della privacy, MIT Private Kit è la nuova generazione di registrazione privata della localizzazione.", + "next":"PROSSIMO", + "back":"INDIETRO", + "start":"INIZIA", + "intro2_title1":"Meno di 100KB", + "intro2_para1":"I movimenti salvati da Private Kit occupano meno di 100KB di spazio – meno di una singola foto.", + "intro2_title2":"Sei tu che decidi", + "intro2_para2":"I tuoi dati non lasceranno mai il tuo telefono senza il tuo consenso", + "intro3_title1":"Il Futuro", + "intro3_para1":"Il prossimo passo per risolvere i problemi di oggi e di domani consiste nel permettere agli individui di salvare i propri movimenti, offrendo nuove opportunita' per ricercatori che studiano pandemie, migrazioni e spostamenti.", + "intro3_para2":"Scopri di piu' http://privatekit.mit.edu/" +} diff --git a/app/locales/it/locationTracking.json b/app/locales/it/locationTracking.json index c870947155..619e82b9de 100644 --- a/app/locales/it/locationTracking.json +++ b/app/locales/it/locationTracking.json @@ -1,12 +1,12 @@ { - "start_logging":"START LOGGING", - "stop_logging":"STOP LOGGING", - "logging_message":"It is currently logging your location privately every five minutes. Your location information will NOT leave your phone.", - "not_logging_message":"NOTE: After clicking this button you may be prompted to grant Private Kit access to your location.", - "import":"Import", - "export":"Export", - "news":"news", - "latest_news":"Latest News", - "url_info":"For more information visit the Private Kit hompage:", + "start_logging":"INIZIA A SALVARE", + "stop_logging":"SMETTI DI SALVARE", + "logging_message":"La APP sta attualmente salvando i tuoi movimenti in privato ogni 5 minuti. Queste informazioni sono salvate solo nel tuo telefono.", + "not_logging_message":"NOTA: Una volta premuto questo tasto, ti sarà richiesto di garantire l'accesso ai tuoi movimenti a Private Kit.", + "import":"Importa", + "export":"Esporta", + "news":"novità", + "latest_news":"Ultime Novità", + "url_info":"Per più informazioni, visita la homepage di Private Kit:", "private_kit_url":"privatekit.mit.edu" -} \ No newline at end of file +} diff --git a/app/locales/languages.js b/app/locales/languages.js index d126c0b4a3..25cc371b5c 100644 --- a/app/locales/languages.js +++ b/app/locales/languages.js @@ -3,66 +3,77 @@ import { getLanguages } from 'react-native-i18n'; // Refer this for checking the codes and creating new folders https://developer.chrome.com/webstore/i18n // Step 1 - Create index.js files for each language we want to have, in this file you can import all the json files (Step 4) and export them -// Step 2 - Import them with a unique title +// Step 2 - Import them with a unique title // Step 3 - Add these titles under the resources object in the i18next.init function // Step 4 - Create separate json files for various sections under the language folder ex. en/intro1.json -// Step 5 - Add the labels to be used in repective json files. The labels are the key and the content is the value in different language, so make sure for each file the key remains the same +// Step 5 - Add the labels to be used in respective json files. The labels are the key and the content is the value in different language, so make sure for each file the key remains the same // Step 6 - In React Native code import the main languages file and call the translate function - languages.t('label.labelname') -import enlabels from './en'; +import enlabels from './en'; import delabels from './de'; import hilabels from './hi'; import frlabels from './fr'; import itlabels from './it'; import ptlabels from './pt'; - +import mrlabels from './mr'; +import nllabels from './nl'; // This will fetch the user's language let userLang = undefined; getLanguages().then(languages => { userLang = languages[0].split('-')[0]; // ['en-US' will become 'en'] - i18next.changeLanguage(userLang); + i18next.changeLanguage(userLang); }); i18next.init({ interpolation: { // React already does escaping - escapeValue: false + escapeValue: false, }, lng: userLang, // 'en' | 'es', - fallbackLng: 'en', // If language detector fails + fallbackLng: 'en', // If language detector fails resources: { en: { translation: { - label: enlabels - } + label: enlabels, + }, }, de: { translation: { - label: delabels - } + label: delabels, + }, }, hi: { translation: { - label: hilabels - } + label: hilabels, + }, }, fr: { translation: { - label: frlabels - } + label: frlabels, + }, }, it: { translation: { - label: itlabels - } + label: itlabels, + }, }, pt: { translation: { label: ptlabels } - } - } + }, + mr: { + translation: { + label: mrlabels + } + }, + nl: { + translation: { + label: nllabels, + }, + }, + }, }); export default i18next; diff --git a/app/locales/mr/exportscreen.json b/app/locales/mr/exportscreen.json new file mode 100644 index 0000000000..493113fea0 --- /dev/null +++ b/app/locales/mr/exportscreen.json @@ -0,0 +1,6 @@ +{ + "export_para_1": "आपण खाली दिलेले शेयर बटण वापरून कुणाबरोबर ही आपली लोकेशन हिस्टरी शेयर करू शकता. बटण दाबले की आपल्याला लोकेशन हिस्टरी कुणाबरोबर आणि कशी शेयर करायची याबाबत सूचना मिळेल.", + "export_para_2": "लोकेशन हिस्टरी ही फक्त वेळ आणि लोकेशन कोऑर्डीनेटच्या स्वरूपात शेयर केली जात आहे. तुमची कोणतीही वैयक्तिक / खाजगी माहिती शेयर केली जात नाही.", + "share": "शेयर", + "data_hint": "लॉग चा डेटा:" +} diff --git a/app/locales/mr/import.json b/app/locales/mr/import.json new file mode 100644 index 0000000000..0a76b51517 --- /dev/null +++ b/app/locales/mr/import.json @@ -0,0 +1,5 @@ +{ + "import_title": "इम्पोर्ट", + "import_step_1":"1. आपल्या गूगल खात्यात लॉग इन करा आणि आपली लोकेशन हिस्टरी डाउनलोड करा.", + "import_step_2":"2. डाउनलोड झाल्यावर हा स्क्रीन परत उघडा. डेटा आपोआप इम्पोर्ट होईल." +} diff --git a/app/locales/mr/index.js b/app/locales/mr/index.js new file mode 100644 index 0000000000..77d79a8a10 --- /dev/null +++ b/app/locales/mr/index.js @@ -0,0 +1,11 @@ +import intro from './intro.json'; +import locationTracking from './locationTracking.json'; +import importFile from './import.json' +import exportFile from './exportscreen.json' + +export default { + ...intro, + ...locationTracking, + ...importFile, + ...exportFile +}; diff --git a/app/locales/mr/intro.json b/app/locales/mr/intro.json new file mode 100644 index 0000000000..ff882ba417 --- /dev/null +++ b/app/locales/mr/intro.json @@ -0,0 +1,14 @@ +{ + "private_kit": "प्राइवेट किट", + "intro1_para1": "सुरक्षा आणि गोपनीयता संरक्षणासह डिझाइन केलेले, एमआयटी प्रायव्हेट किट सुरक्षित लोकेशन लॉगिंगची पुढील पिढी आहे.", + "next": "पुढे", + "back": "मागे", + "start": "सुरु करा", + "intro2_title1": "100KB पेक्षा कमी", + "intro2_para1": "प्राइवेट किट चा ट्रेल जनरेटर आपल्या मोबाईलचा लोकेशन-डेटा फक्त 100KB मध्ये स्टोअर करतो, जी जागा एका photo पेक्षाही कमी आहे.", + "intro2_title2": "आपण आपला स्वतःचा डेटा नियंत्रित करू शकता.", + "intro2_para2": "आपला लोकेशन डेटा आपल्या संमतीशिवाय कधीही वापरला जाणार नाही.", + "intro3_title1": "भविष्य", + "intro3_para1": "भावी समस्यांच्या निराकरणासाठी, लोकांना आपले लोकेशन नोंद करण्यात सक्षम करणारे आणि साथीचा आजारांचे ट्रैकिंग, रेफ्युजी मायग्रेशन, आणि कम्युनिटी ट्रॅफिक अॅनॅलिसिस चा अभ्यास करणाऱ्या संशोधकांना हे नवीन अॅप मदत करते.", + "intro3_para2": "अधिक माहितीसाठी लॉग इन करा: http://privatekit.mit.edu/" +} diff --git a/app/locales/mr/locationTracking.json b/app/locales/mr/locationTracking.json new file mode 100644 index 0000000000..6e05416752 --- /dev/null +++ b/app/locales/mr/locationTracking.json @@ -0,0 +1,12 @@ +{ + "start_logging": "स्टार्ट लॉगिंग", + "stop_logging": "स्टॉप लॉगिंग", + "logging_message": "हे सध्या दर पाच मिनिटांनी आपले लोकेशन खाजगीरित्या लॉग करत आहे. आपली लोकेशन माहिती आपला फोन सोडणार नाही.", + "not_logging_message": "सुचना: या बटणावर क्लिक केल्यानंतर प्राइवेट किट आपणास आपले लोकेशन ट्रॅक करण्याची परवानगी मागेल.", + "import": "इम्पोर्ट", + "export": "एक्स्पोर्ट", + "news": "बातम्या", + "latest_news": "ताज्या बातम्या", + "url_info": "अधिक माहितीसाठी प्राइवेट किट होम पेज ला भेट द्या:", + "private_kit_url": "privatekit.mit.edu" +} diff --git a/app/locales/nl/exportscreen.json b/app/locales/nl/exportscreen.json new file mode 100644 index 0000000000..80dc1db942 --- /dev/null +++ b/app/locales/nl/exportscreen.json @@ -0,0 +1,6 @@ +{ + "export_para_1":"Je kunt jouw locatiehistorie met iedereen delen via de knop Deel hieronder. Zodra je op de knop drukt, wordt je gevraagd met wie en hoe je deze wilt delen.", + "export_para_2":"Je locatiehistorie wordt gedeeld als een eenvoudige lijst met tijden en coördinaten, zonder andere identificerende informatie.", + "share":"DEEL", + "data_hint":"Log bevat data van" +} \ No newline at end of file diff --git a/app/locales/nl/import.json b/app/locales/nl/import.json new file mode 100644 index 0000000000..f11febba09 --- /dev/null +++ b/app/locales/nl/import.json @@ -0,0 +1,5 @@ +{ + "import_title":"Importeer Locaties", + "import_step_1":"1. Log in op je Google Account en download jouw locatiehistorie", + "import_step_2":"2. Na het downloaden, open dit scherm opnieuw. De data wordt dan automatisch geïmporteerd." +} \ No newline at end of file diff --git a/app/locales/nl/index.js b/app/locales/nl/index.js new file mode 100644 index 0000000000..3b0f20127c --- /dev/null +++ b/app/locales/nl/index.js @@ -0,0 +1,13 @@ +import intro from './intro.json'; +import locationTracking from './locationTracking.json'; +import importFile from './import.json'; +import exportFile from './exportscreen.json'; +import licensesFile from './licensesscreen.json'; + +export default { + ...intro, + ...locationTracking, + ...importFile, + ...exportFile, + ...licensesFile, +}; diff --git a/app/locales/nl/intro.json b/app/locales/nl/intro.json new file mode 100644 index 0000000000..b65c476747 --- /dev/null +++ b/app/locales/nl/intro.json @@ -0,0 +1,14 @@ +{ + "private_kit":"Private Kit", + "intro1_para1":"Ontworpen met gegevensbeveiliging en privacybescherming als basis. MIT Private Kit is de volgende generatie van veilige locatietracking.", + "next":"VOLGENDE", + "back":"TERUG", + "start":"START", + "intro2_title1":"Minder dan 100KB", + "intro2_para1":"Private Kit gebruikt minder dan 100KB om je locatiehistorie op te slaan – minder dan de ruimte van één foto dus.", + "intro2_title2":"Jij houdt de controle", + "intro2_para2":"Data verlaat nooit je telefoon zonder jouw expliciete toestemming.", + "intro3_title1":"De toekomst", + "intro3_para1":"De volgende stap in het oplossen van de problemen van vandaag en morgen. Mensen in staat stellen hun locatiehistorie bij te houden, biedt nieuwe kansen voor onderzoekers die de verspreiding van pandemiën, vluchtelingenmigratie en gemeenschapsverkeer bestuderen.", + "intro3_para2":"Kom meer te weten:\n http://privatekit.mit.edu/" +} \ No newline at end of file diff --git a/app/locales/nl/licensesscreen.json b/app/locales/nl/licensesscreen.json new file mode 100644 index 0000000000..e14754d6ba --- /dev/null +++ b/app/locales/nl/licensesscreen.json @@ -0,0 +1,3 @@ +{ + "license_placeholder": "Deze app is gebouwd onder de MIT licentie." +} \ No newline at end of file diff --git a/app/locales/nl/locationTracking.json b/app/locales/nl/locationTracking.json new file mode 100644 index 0000000000..98532504ff --- /dev/null +++ b/app/locales/nl/locationTracking.json @@ -0,0 +1,12 @@ +{ + "start_logging":"START OPSLAAN", + "stop_logging":"STOP OPSLAAN", + "logging_message":"De app slaat jouw locatie op dit moment elke vijf minuten privé op. Je locatieinformatie wordt NIET automatisch gedeeld vanaf je telefoon.", + "not_logging_message":"OPMERKING: Nadat je op deze knop hebt gedrukt, wordt je mogelijk gevraagd Private Kit toegang te verlenen tot jouw locatie.", + "import":"Importeren", + "export":"Exporteren", + "news":"Nieuws", + "latest_news":"Laatste nieuws", + "url_info":"Voor meer informatie bezoek de Private Kit website:", + "private_kit_url":"privatekit.mit.edu" +} \ No newline at end of file diff --git a/app/locales/pt/exportscreen.json b/app/locales/pt/exportscreen.json index d834b2f95a..de6016af95 100644 --- a/app/locales/pt/exportscreen.json +++ b/app/locales/pt/exportscreen.json @@ -1,6 +1,6 @@ { - "export_para_1":"You can share your location trail with anyone using the Share button below. Once you press the button it will ask you with whom and how you want to share it.", - "export_para_2":"Location is shared as a simple list of times and coordinates, no other identifying information.", - "share":"SHARE", - "data_hint":"Log has data of" + "export_para_1":"Você pode compartilhar o seu histórico de localização com qualquer pessoa usando o botão de Compartilhar. Depois de pressionar o botão, você escolhe com quem e como deseja compartilhar.", + "export_para_2":"Seu histórico de localização é compartilhado como uma lista simples de horários e coordenadas sem nenhuma outra informação de identificação.", + "share":"COMPARTILHAR", + "data_hint":"Registros de" } \ No newline at end of file diff --git a/app/locales/pt/import.json b/app/locales/pt/import.json index 58e337de66..579ebf85f6 100644 --- a/app/locales/pt/import.json +++ b/app/locales/pt/import.json @@ -1,5 +1,5 @@ { - "import_title":"Import Locations", - "import_step_1":"1. Login to your Google Account and Download your Location History", - "import_step_2":"2. After downloaded, open this screen again. The data will import automatically." + "import_title":"Importar histórico", + "import_step_1":"1. Faça o login na sua conta do Google e baixe seu histórico de localização", + "import_step_2":"2. Depois de baixar, abra esta tela novamente que os dados serão importados automaticamente." } \ No newline at end of file diff --git a/app/locales/pt/index.js b/app/locales/pt/index.js index 77d79a8a10..c63e419c7e 100644 --- a/app/locales/pt/index.js +++ b/app/locales/pt/index.js @@ -1,11 +1,11 @@ import intro from './intro.json'; import locationTracking from './locationTracking.json'; -import importFile from './import.json' -import exportFile from './exportscreen.json' +import importFile from './import.json'; +import exportFile from './exportscreen.json'; export default { - ...intro, - ...locationTracking, - ...importFile, - ...exportFile + ...intro, + ...locationTracking, + ...importFile, + ...exportFile, }; diff --git a/app/locales/pt/intro.json b/app/locales/pt/intro.json index 9e1f07ae35..c61ea0f7ba 100644 --- a/app/locales/pt/intro.json +++ b/app/locales/pt/intro.json @@ -1,14 +1,14 @@ { "private_kit":"Private Kit", - "intro1_para1":"Designed with data security and privacy protection at its heart, MIT Private Kit is the next generation of secure location logging.", - "next":"NEXT", - "back":"BACK", - "start":"START", - "intro2_title1":"Less than 100KB", - "intro2_para1":"Private Kit’s trail generator logs your device’s location data in under 100KB of space – less space than a single picture.", - "intro2_title2":"You are in charge", - "intro2_para2":"Data Never Leaves Your Device Without Your Consent", - "intro3_title1":"The Future", - "intro3_para1":"The Next Step in Solving Today and Tomorrow’s Problems Enabling individuals to log their location trail offers new opportunities for researchers studying pandemic tracking, refugee migration, and community traffic analysis.", - "intro3_para2":"Learn More http://privatekit.mit.edu/" + "intro1_para1":"Projetado com segurança de dados e proteção de privacidade, o MIT Private Kit é a próxima geração segura para históricos de localização.", + "next":"PRÓXIMO", + "back":"VOLTAR", + "start":"INICIAR", + "intro2_title1":"Menos de 100KB", + "intro2_para1":"O Private Kit’s registra os dados de localização do seu dispositivo em menos de 100 KB de espaço - isso é menor do que uma foto.", + "intro2_title2":"Você está no controle", + "intro2_para2":"Os dados nunca saem do seu dispositivo sem o seu consentimento", + "intro3_title1":"O futuro", + "intro3_para1":"A próxima etapa na solução dos problemas de hoje e de amanhã. O histórico de localização de indivíduos oferece novas oportunidades para os pesquisadores que estudam rastreamento de pandemia, migração de refugiados e análise de tráfego da comunidade.", + "intro3_para2":"Saiba mais em http://privatekit.mit.edu/" } \ No newline at end of file diff --git a/app/locales/pt/locationTracking.json b/app/locales/pt/locationTracking.json index c870947155..6b5cffec67 100644 --- a/app/locales/pt/locationTracking.json +++ b/app/locales/pt/locationTracking.json @@ -1,12 +1,12 @@ { - "start_logging":"START LOGGING", - "stop_logging":"STOP LOGGING", - "logging_message":"It is currently logging your location privately every five minutes. Your location information will NOT leave your phone.", - "not_logging_message":"NOTE: After clicking this button you may be prompted to grant Private Kit access to your location.", - "import":"Import", - "export":"Export", - "news":"news", - "latest_news":"Latest News", - "url_info":"For more information visit the Private Kit hompage:", + "start_logging":"INICIAR", + "stop_logging":"PARAR", + "logging_message":"Sua localização atual está sendo registrada a cada cinco minutos. Suas informações de localização NÃO sairão do seu telefone.", + "not_logging_message":"NOTA: Depois de clicar neste botão, você pode ser solicitado para liberar o acesso da sua localização ao Private Kit.", + "import":"Importar", + "export":"Exportar", + "news":"Notícias", + "latest_news":"Últimas notícias", + "url_info":"Para mais informações sobre o Private Kit acesse a página:", "private_kit_url":"privatekit.mit.edu" } \ No newline at end of file diff --git a/app/services/LocationService.js b/app/services/LocationService.js index eca6c72c8f..df9ab2f6c7 100644 --- a/app/services/LocationService.js +++ b/app/services/LocationService.js @@ -1,116 +1,115 @@ -import { - GetStoreData, - SetStoreData -} from '../helpers/General'; +import { GetStoreData, SetStoreData } from '../helpers/General'; import BackgroundGeolocation from '@mauron85/react-native-background-geolocation'; import { Alert } from 'react-native'; -import PushNotificationIOS from "@react-native-community/push-notification-ios"; +import PushNotificationIOS from '@react-native-community/push-notification-ios'; -import PushNotification from "react-native-push-notification"; +import PushNotification from 'react-native-push-notification'; var instanceCount = 0; var lastPointCount = 0; -var locationInterval = 60000 * 5; // Time (in milliseconds) between location information polls. E.g. 60000*5 = 5 minutes +var locationInterval = 60000 * 5; // Time (in milliseconds) between location information polls. E.g. 60000*5 = 5 minutes // DEBUG: Reduce Time intervall for faster debugging // var locationInterval = 5000; function saveLocation(location) { - // Persist this location data in our local storage of time/lat/lon values - - GetStoreData('LOCATION_DATA') - .then(locationArrayString => { - - var locationArray; - if (locationArrayString !== null) { - locationArray = JSON.parse(locationArrayString); - } else { - locationArray = []; - } - - // Always work in UTC, not the local time in the locationData - var nowUTC = new Date().toISOString(); - var unixtimeUTC = Date.parse(nowUTC); - var unixtimeUTC_28daysAgo = unixtimeUTC - (60 * 60 * 24 * 1000 * 28); - - // Curate the list of points, only keep the last 28 days - var curated = []; - for (var i = 0; i < locationArray.length; i++) { - if (locationArray[i]["time"] > unixtimeUTC_28daysAgo) { - curated.push(locationArray[i]); - } - } - - // Backfill the stationary points, if available - if (curated.length >= 1) { - var lastLocationArray = curated[curated.length - 1]; - var lastTS = lastLocationArray["time"]; - for (; lastTS < unixtimeUTC - locationInterval; lastTS += locationInterval) { - curated.push(JSON.parse(JSON.stringify(lastLocationArray))); - } - } - - // Save the location using the current lat-lon and the - // calculated UTC time (maybe a few milliseconds off from - // when the GPS data was collected, but that's unimportant - // for what we are doing.) - lastPointCount = locationArray.length; - console.log('[GPS] Saving point:', lastPointCount) - var lat_lon_time = { - "latitude": location["latitude"], - "longitude": location["longitude"], - "time": unixtimeUTC - }; - curated.push(lat_lon_time); - - SetStoreData('LOCATION_DATA', curated); - }); -} + // Persist this location data in our local storage of time/lat/lon values + + GetStoreData('LOCATION_DATA').then(locationArrayString => { + var locationArray; + if (locationArrayString !== null) { + locationArray = JSON.parse(locationArrayString); + } else { + locationArray = []; + } + // Always work in UTC, not the local time in the locationData + var nowUTC = new Date().toISOString(); + var unixtimeUTC = Date.parse(nowUTC); + var unixtimeUTC_28daysAgo = unixtimeUTC - 60 * 60 * 24 * 1000 * 28; + + // Curate the list of points, only keep the last 28 days + var curated = []; + for (var i = 0; i < locationArray.length; i++) { + if (locationArray[i]['time'] > unixtimeUTC_28daysAgo) { + curated.push(locationArray[i]); + } + } + + // Backfill the stationary points, if available + if (curated.length >= 1) { + var lastLocationArray = curated[curated.length - 1]; + var lastTS = lastLocationArray['time']; + for ( + ; + lastTS < unixtimeUTC - locationInterval; + lastTS += locationInterval + ) { + curated.push(JSON.parse(JSON.stringify(lastLocationArray))); + } + } + + // Save the location using the current lat-lon and the + // calculated UTC time (maybe a few milliseconds off from + // when the GPS data was collected, but that's unimportant + // for what we are doing.) + lastPointCount = locationArray.length; + console.log('[GPS] Saving point:', lastPointCount); + var lat_lon_time = { + latitude: location['latitude'], + longitude: location['longitude'], + time: unixtimeUTC, + }; + curated.push(lat_lon_time); + + SetStoreData('LOCATION_DATA', curated); + }); +} export default class LocationServices { - static start() { - instanceCount += 1 - if (instanceCount > 1) { - BackgroundGeolocation.start(); - return; - } + static start() { + instanceCount += 1; + if (instanceCount > 1) { + BackgroundGeolocation.start(); + return; + } - PushNotification.configure({ - // (required) Called when a remote or local notification is opened or received - onNotification: function(notification) { - console.log("NOTIFICATION:", notification); - // required on iOS only (see fetchCompletionHandler docs: https://github.com/react-native-community/react-native-push-notification-ios) - notification.finish(PushNotificationIOS.FetchResult.NoData); - }, - requestPermissions: true - }); - - // PushNotificationIOS.requestPermissions(); - BackgroundGeolocation.configure({ - desiredAccuracy: BackgroundGeolocation.HIGH_ACCURACY, - stationaryRadius: 5, - distanceFilter: 5, - notificationTitle: 'Private Kit Enabled', - notificationText: 'Private Kit is securely storing your GPS coordinates once every five minutes on this device.', - debug: false, // when true, it beeps every time a loc is read - startOnBoot: true, - stopOnTerminate: false, - locationProvider: BackgroundGeolocation.DISTANCE_FILTER_PROVIDER, - - interval: locationInterval, - fastestInterval: locationInterval, - activitiesInterval: locationInterval, - - activityType: "AutomotiveNavigation", - pauseLocationUpdates: false, - saveBatteryOnBackground: true, - stopOnStillActivity: false, - }); - - BackgroundGeolocation.on('location', (location) => { - // handle your locations here - /* SAMPLE OF LOCATION DATA OBJECT + PushNotification.configure({ + // (required) Called when a remote or local notification is opened or received + onNotification: function(notification) { + console.log('NOTIFICATION:', notification); + // required on iOS only (see fetchCompletionHandler docs: https://github.com/react-native-community/react-native-push-notification-ios) + notification.finish(PushNotificationIOS.FetchResult.NoData); + }, + requestPermissions: true, + }); + + // PushNotificationIOS.requestPermissions(); + BackgroundGeolocation.configure({ + desiredAccuracy: BackgroundGeolocation.HIGH_ACCURACY, + stationaryRadius: 5, + distanceFilter: 5, + notificationTitle: 'Private Kit Enabled', + notificationText: + 'Private Kit is securely storing your GPS coordinates once every five minutes on this device.', + debug: false, // when true, it beeps every time a loc is read + startOnBoot: true, + stopOnTerminate: false, + locationProvider: BackgroundGeolocation.DISTANCE_FILTER_PROVIDER, + + interval: locationInterval, + fastestInterval: locationInterval, + activitiesInterval: locationInterval, + + activityType: 'AutomotiveNavigation', + pauseLocationUpdates: false, + saveBatteryOnBackground: true, + stopOnStillActivity: false, + }); + + BackgroundGeolocation.on('location', location => { + // handle your locations here + /* SAMPLE OF LOCATION DATA OBJECT { "accuracy": 20, "altitude": 5, "id": 114, "isFromMockProvider": false, "latitude": 37.4219983, "locationProvider": 1, "longitude": -122.084, @@ -118,174 +117,204 @@ export default class LocationServices { "time": 1583696413000 } */ - // to perform long running operation on iOS - // you need to create background task - BackgroundGeolocation.startTask(taskKey => { - // execute long running task - // eg. ajax post location - // IMPORTANT: task has to be ended by endTask - saveLocation(location); - BackgroundGeolocation.endTask(taskKey); - }); - }); - - if (Platform.OS === 'android') { - // This feature only is present on Android. - BackgroundGeolocation.headlessTask(async (event) => { - // Application was shutdown, but the headless mechanism allows us - // to capture events in the background. (On Android, at least) - if (event.name === 'location' || event.name === 'stationary') { - saveLocation(event.params); - } - }); + // to perform long running operation on iOS + // you need to create background task + BackgroundGeolocation.startTask(taskKey => { + // execute long running task + // eg. ajax post location + // IMPORTANT: task has to be ended by endTask + saveLocation(location); + BackgroundGeolocation.endTask(taskKey); + }); + }); + + if (Platform.OS === 'android') { + // This feature only is present on Android. + BackgroundGeolocation.headlessTask(async event => { + // Application was shutdown, but the headless mechanism allows us + // to capture events in the background. (On Android, at least) + if (event.name === 'location' || event.name === 'stationary') { + saveLocation(event.params); } - - BackgroundGeolocation.on('stationary', (stationaryLocation) => { - // handle stationary locations here - // Actions.sendLocation(stationaryLocation); - BackgroundGeolocation.startTask(taskKey => { - // execute long running task - // eg. ajax post location - // IMPORTANT: task has to be ended by endTask - - // For capturing stationaryLocation. Note that it hasn't been - // tested as I couldn't produce stationaryLocation callback in emulator - // but since the plugin documentation mentions it, no reason to keep - // it empty I believe. - saveLocation(stationaryLocation); - BackgroundGeolocation.endTask(taskKey); - }); - console.log('[INFO] stationaryLocation:', stationaryLocation); - }); - - BackgroundGeolocation.on('error', (error) => { - console.log('[ERROR] BackgroundGeolocation error:', error); - }); - - BackgroundGeolocation.on('start', () => { - console.log('[INFO] BackgroundGeolocation service has been started'); - }); - - BackgroundGeolocation.on('stop', () => { - console.log('[INFO] BackgroundGeolocation service has been stopped'); - }); - - BackgroundGeolocation.on('authorization', (status) => { - console.log('[INFO] BackgroundGeolocation authorization status: ' + status); - - if (status !== BackgroundGeolocation.AUTHORIZED) { - // we need to set delay or otherwise alert may not be shown - setTimeout(() => - Alert.alert('Private Kit requires access to location information', 'Would you like to open app settings?', [{ - text: 'Yes', - onPress: () => BackgroundGeolocation.showAppSettings() - }, - { - text: 'No', - onPress: () => console.log('No Pressed'), - style: 'cancel' - } - ]), 1000); - } - else { - BackgroundGeolocation.start(); //triggers start on start event - - // TODO: We reach this point on Android when location services are toggled off/on. - // When this fires, check if they are off and show a Notification in the tray - } - }); - - BackgroundGeolocation.on('background', () => { - console.log('[INFO] App is in background'); - }); - - BackgroundGeolocation.on('foreground', () => { - console.log('[INFO] App is in foreground'); - }); - - BackgroundGeolocation.on('abort_requested', () => { - console.log('[INFO] Server responded with 285 Updates Not Required'); - // Here we can decide whether we want stop the updates or not. - // If you've configured the server to return 285, then it means the server does not require further update. - // So the normal thing to do here would be to `BackgroundGeolocation.stop()`. - // But you might be counting on it to receive location updates in the UI, so you could just reconfigure and set `url` to null. - }); - - BackgroundGeolocation.on('http_authorization', () => { - console.log('[INFO] App needs to authorize the http requests'); - }); - - BackgroundGeolocation.on('stop', () => { - PushNotification.localNotification({ - title: "Location Tracking Was Disabled", - message: "Private Kit requires location services." - }); - console.log('[INFO] stop'); - }); - - BackgroundGeolocation.on('stationary', () => { - console.log('[INFO] stationary'); - }); - - BackgroundGeolocation.checkStatus(status => { - console.log('[INFO] BackgroundGeolocation service is running', status.isRunning); - console.log('[INFO] BackgroundGeolocation services enabled', status.locationServicesEnabled); - console.log('[INFO] BackgroundGeolocation auth status: ' + status.authorization); - - BackgroundGeolocation.start(); //triggers start on start event - - if (!status.locationServicesEnabled) { - // we need to set delay or otherwise alert may not be shown - setTimeout(() => - Alert.alert('Private Kit requires location services to be enabled', 'Would you like to open location settings?', [{ - text: 'Yes', - onPress: () => BackgroundGeolocation.showLocationSettings() - }, - { - text: 'No', - onPress: () => console.log('No Pressed'), - style: 'cancel' - } - ]), 1000); - } - else if (!status.authorization) { - // we need to set delay or otherwise alert may not be shown - setTimeout(() => - Alert.alert('Private Kit requires access to location information', 'Would you like to open app settings?', [{ - text: 'Yes', - onPress: () => BackgroundGeolocation.showAppSettings() - }, - { - text: 'No', - onPress: () => console.log('No Pressed'), - style: 'cancel' - } - ]), 1000); - } - else if (!status.isRunning) { - } - - }); - - // you can also just start without checking for status - // BackgroundGeolocation.start(); - } - - static getPointCount() { - return lastPointCount; + }); } - static stop(nav) { - // unregister all event listeners - PushNotification.localNotification({ - title: "Location Tracking Was Disabled", - message: "Private Kit requires location services." - }); - BackgroundGeolocation.removeAllListeners(); - BackgroundGeolocation.stop(); - instanceCount -= 1; - SetStoreData('PARTICIPATE', 'false').then(() => - nav.navigate('LocationTrackingScreen', {}) - ) - } + BackgroundGeolocation.on('stationary', stationaryLocation => { + // handle stationary locations here + // Actions.sendLocation(stationaryLocation); + BackgroundGeolocation.startTask(taskKey => { + // execute long running task + // eg. ajax post location + // IMPORTANT: task has to be ended by endTask + + // For capturing stationaryLocation. Note that it hasn't been + // tested as I couldn't produce stationaryLocation callback in emulator + // but since the plugin documentation mentions it, no reason to keep + // it empty I believe. + saveLocation(stationaryLocation); + BackgroundGeolocation.endTask(taskKey); + }); + console.log('[INFO] stationaryLocation:', stationaryLocation); + }); + + BackgroundGeolocation.on('error', error => { + console.log('[ERROR] BackgroundGeolocation error:', error); + }); + + BackgroundGeolocation.on('start', () => { + console.log('[INFO] BackgroundGeolocation service has been started'); + }); + + BackgroundGeolocation.on('stop', () => { + console.log('[INFO] BackgroundGeolocation service has been stopped'); + }); + + BackgroundGeolocation.on('authorization', status => { + console.log( + '[INFO] BackgroundGeolocation authorization status: ' + status, + ); + + if (status !== BackgroundGeolocation.AUTHORIZED) { + // we need to set delay or otherwise alert may not be shown + setTimeout( + () => + Alert.alert( + 'Private Kit requires access to location information', + 'Would you like to open app settings?', + [ + { + text: 'Yes', + onPress: () => BackgroundGeolocation.showAppSettings(), + }, + { + text: 'No', + onPress: () => console.log('No Pressed'), + style: 'cancel', + }, + ], + ), + 1000, + ); + } else { + BackgroundGeolocation.start(); //triggers start on start event + + // TODO: We reach this point on Android when location services are toggled off/on. + // When this fires, check if they are off and show a Notification in the tray + } + }); + + BackgroundGeolocation.on('background', () => { + console.log('[INFO] App is in background'); + }); + + BackgroundGeolocation.on('foreground', () => { + console.log('[INFO] App is in foreground'); + }); + + BackgroundGeolocation.on('abort_requested', () => { + console.log('[INFO] Server responded with 285 Updates Not Required'); + // Here we can decide whether we want stop the updates or not. + // If you've configured the server to return 285, then it means the server does not require further update. + // So the normal thing to do here would be to `BackgroundGeolocation.stop()`. + // But you might be counting on it to receive location updates in the UI, so you could just reconfigure and set `url` to null. + }); + + BackgroundGeolocation.on('http_authorization', () => { + console.log('[INFO] App needs to authorize the http requests'); + }); + + BackgroundGeolocation.on('stop', () => { + PushNotification.localNotification({ + title: 'Location Tracking Was Disabled', + message: 'Private Kit requires location services.', + }); + console.log('[INFO] stop'); + }); + + BackgroundGeolocation.on('stationary', () => { + console.log('[INFO] stationary'); + }); + + BackgroundGeolocation.checkStatus(status => { + console.log( + '[INFO] BackgroundGeolocation service is running', + status.isRunning, + ); + console.log( + '[INFO] BackgroundGeolocation services enabled', + status.locationServicesEnabled, + ); + console.log( + '[INFO] BackgroundGeolocation auth status: ' + status.authorization, + ); + + BackgroundGeolocation.start(); //triggers start on start event + + if (!status.locationServicesEnabled) { + // we need to set delay or otherwise alert may not be shown + setTimeout( + () => + Alert.alert( + 'Private Kit requires location services to be enabled', + 'Would you like to open location settings?', + [ + { + text: 'Yes', + onPress: () => BackgroundGeolocation.showLocationSettings(), + }, + { + text: 'No', + onPress: () => console.log('No Pressed'), + style: 'cancel', + }, + ], + ), + 1000, + ); + } else if (!status.authorization) { + // we need to set delay or otherwise alert may not be shown + setTimeout( + () => + Alert.alert( + 'Private Kit requires access to location information', + 'Would you like to open app settings?', + [ + { + text: 'Yes', + onPress: () => BackgroundGeolocation.showAppSettings(), + }, + { + text: 'No', + onPress: () => console.log('No Pressed'), + style: 'cancel', + }, + ], + ), + 1000, + ); + } else if (!status.isRunning) { + } + }); + + // you can also just start without checking for status + // BackgroundGeolocation.start(); + } + + static getPointCount() { + return lastPointCount; + } + + static stop(nav) { + // unregister all event listeners + PushNotification.localNotification({ + title: 'Location Tracking Was Disabled', + message: 'Private Kit requires location services.', + }); + BackgroundGeolocation.removeAllListeners(); + BackgroundGeolocation.stop(); + instanceCount -= 1; + SetStoreData('PARTICIPATE', 'false').then(() => + nav.navigate('LocationTrackingScreen', {}), + ); + } } diff --git a/app/views/Export.js b/app/views/Export.js index 2362d00765..2dc7a9f988 100644 --- a/app/views/Export.js +++ b/app/views/Export.js @@ -1,204 +1,234 @@ -import React, { - Component -} from 'react'; +import React, { Component } from 'react'; import { - SafeAreaView, - StyleSheet, - ScrollView, - Linking, - View, - Text, - Image, - Dimensions, - TouchableOpacity,BackHandler + SafeAreaView, + StyleSheet, + ScrollView, + Linking, + View, + Text, + Image, + Platform, + Dimensions, + TouchableOpacity, + BackHandler, } from 'react-native'; -import colors from "../constants/colors"; -import WebView from 'react-native-webview'; -import Button from "../components/Button"; -import { - GetStoreData -} from '../helpers/General'; -import { - convertPointsToString -} from '../helpers/convertPointsToString'; +import colors from '../constants/colors'; +import { GetStoreData } from '../helpers/General'; +import { convertPointsToString } from '../helpers/convertPointsToString'; import Share from 'react-native-share'; import RNFetchBlob from 'rn-fetch-blob'; import LocationServices from '../services/LocationService'; -import backArrow from './../assets/images/backArrow.png' -import languages from './../locales/languages' +import backArrow from './../assets/images/backArrow.png'; +import languages from './../locales/languages'; const width = Dimensions.get('window').width; -const base64 = RNFetchBlob.base64 - - -//import RNShareFile from 'react-native-file-share'; +const base64 = RNFetchBlob.base64; +// require the module +// var RNFS = require('react-native-fs'); class ExportScreen extends Component { - constructor(props) { - super(props); - } - - onShare = async () => { - try { - const locationArray = await GetStoreData('LOCATION_DATA'); - var locationData; - - if (locationArray !== null) { - locationData = JSON.parse(locationArray); - } else { - locationData = []; - } - - b64Data = base64.encode(JSON.stringify(locationData)); - Share.open({ - url: "data:string/txt;base64," + b64Data - }).then(res => { - console.log(res); - }) - .catch(err => { - console.log(err.message, err.code); - }) - } catch (error) { - console.log(error.message); - } - }; - - backToMain() { - this.props.navigation.navigate('LocationTrackingScreen', {}) - } - - handleBackPress = () => { - this.props.navigation.navigate('LocationTrackingScreen', {}); - return true; - }; - - componentDidMount() { - BackHandler.addEventListener("hardwareBackPress", this.handleBackPress); - } - - componentWillUnmount() { - BackHandler.removeEventListener("hardwareBackPress", this.handleBackPress); - } - - render() { - - return ( - - - - this.backToMain()}> - - - {languages.t('label.export')} - - - - {languages.t('label.export_para_1')} - {languages.t('label.export_para_2')} - - {languages.t('label.share')} - - {languages.t('label.data_hint')} {convertPointsToString(LocationServices.getPointCount()) } - - - - ) + constructor(props) { + super(props); + } + + OnShare = async () => { + try { + const locationArray = await GetStoreData('LOCATION_DATA'); + var locationData; + + if (locationArray !== null) { + locationData = JSON.parse(locationArray); + } else { + locationData = []; + } + + const jsonData = base64.encode(JSON.stringify(locationData)); + const title = 'PrivateKit_.json'; + const filename = 'PrivacyKit_.json'; + const message = 'Here is my location log from Private Kit.'; + const url = 'data:application/json;base64,' + jsonData; + const options = Platform.select({ + ios: { + activityItemSources: [ + { + placeholderItem: { type: 'url', content: url }, + item: { + default: { type: 'url', content: url }, + }, + subject: { + default: title, + }, + linkMetadata: { originalUrl: url, url, title }, + }, + { + placeholderItem: { type: 'text', content: message }, + item: { + default: { type: 'text', content: message }, + message: null, // Specify no text to share via Messages app. + }, + }, + ], + }, + default: { + title, + subject: title, + url: url, + message: message, + filename: filename, + }, + }); + + Share.open(options) + .then(res => { + console.log(res); + }) + .catch(err => { + console.log(err.message, err.code); + }); + } catch (error) { + console.log(error.message); } + }; + + backToMain() { + this.props.navigation.navigate('LocationTrackingScreen', {}); + } + + handleBackPress = () => { + this.props.navigation.navigate('LocationTrackingScreen', {}); + return true; + }; + + componentDidMount() { + BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); + } + + componentWillUnmount() { + BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); + } + + render() { + return ( + + + this.backToMain()}> + + + {languages.t('label.export')} + + + + + {languages.t('label.export_para_1')} + + + {languages.t('label.export_para_2')} + + + {languages.t('label.share')} + + + {languages.t('label.data_hint')}{' '} + {convertPointsToString(LocationServices.getPointCount())} + + + + ); + } } const styles = StyleSheet.create({ - // Container covers the entire screen - container: { - flex: 1, - flexDirection: 'column', - color: colors.PRIMARY_TEXT, - backgroundColor: colors.WHITE - }, - headerTitle: { - textAlign: 'center', - fontWeight: "bold", - fontSize: 38, - - padding: 0 - }, - subHeaderTitle: { - textAlign: 'center', - fontWeight: "bold", - fontSize: 22, - padding: 5 - }, - main: { - flex: 1, - flexDirection: 'column', - textAlignVertical: 'top', - // alignItems: 'center', - padding:20, - width:'96%', - alignSelf:'center' - }, - buttonTouchable: { - borderRadius: 12, - backgroundColor: "#665eff", - height:52, - alignSelf:'center', - width:width*.7866, - marginTop:30, - justifyContent:'center' - }, - buttonText:{ - - fontFamily: "OpenSans-Bold", - fontSize: 14, - lineHeight: 19, - letterSpacing: 0, - textAlign: "center", - color: "#ffffff" - }, - mainText: { - fontSize: 18, - lineHeight: 24, - fontWeight: '400', - textAlignVertical: 'center', - padding: 20, - }, - smallText: { - fontSize: 10, - lineHeight: 24, - fontWeight: '400', - textAlignVertical: 'center', - padding: 20, - }, - - headerContainer: { - flexDirection: 'row', - height:60, - borderBottomWidth:1, - borderBottomColor:'rgba(189, 195, 199,0.6)', - alignItems:'center' - }, - backArrowTouchable:{ - width:60, - height:60, - paddingTop:21, - paddingLeft:20 - }, - backArrow: { - height: 18, - width: 18.48 - }, - headerTitle:{ - fontSize: 24, - fontFamily:'OpenSans-Bold' - }, - sectionDescription: { - fontSize: 16, - lineHeight: 24, - marginTop:12, - fontFamily:'OpenSans-Regular', - }, + // Container covers the entire screen + container: { + flex: 1, + flexDirection: 'column', + color: colors.PRIMARY_TEXT, + backgroundColor: colors.WHITE, + }, + headerTitle: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 38, + + padding: 0, + }, + subHeaderTitle: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 22, + padding: 5, + }, + main: { + flex: 1, + flexDirection: 'column', + textAlignVertical: 'top', + // alignItems: 'center', + padding: 20, + width: '96%', + alignSelf: 'center', + }, + buttonTouchable: { + borderRadius: 12, + backgroundColor: '#665eff', + height: 52, + alignSelf: 'center', + width: width * 0.7866, + marginTop: 30, + justifyContent: 'center', + }, + buttonText: { + fontFamily: 'OpenSans-Bold', + fontSize: 14, + lineHeight: 19, + letterSpacing: 0, + textAlign: 'center', + color: '#ffffff', + }, + mainText: { + fontSize: 18, + lineHeight: 24, + fontWeight: '400', + textAlignVertical: 'center', + padding: 20, + }, + smallText: { + fontSize: 10, + lineHeight: 24, + fontWeight: '400', + textAlignVertical: 'center', + padding: 20, + }, + + headerContainer: { + flexDirection: 'row', + height: 60, + borderBottomWidth: 1, + borderBottomColor: 'rgba(189, 195, 199,0.6)', + alignItems: 'center', + }, + backArrowTouchable: { + width: 60, + height: 60, + paddingTop: 21, + paddingLeft: 20, + }, + backArrow: { + height: 18, + width: 18.48, + }, + sectionDescription: { + fontSize: 16, + lineHeight: 24, + marginTop: 12, + fontFamily: 'OpenSans-Regular', + }, }); -export default ExportScreen; \ No newline at end of file +export default ExportScreen; diff --git a/app/views/Import.js b/app/views/Import.js index c6200e3bc7..1e31d9e387 100644 --- a/app/views/Import.js +++ b/app/views/Import.js @@ -1,140 +1,141 @@ -import React, { - Component -} from 'react'; +import React, { Component } from 'react'; import { - SafeAreaView, - StyleSheet, - ScrollView, - Linking, - View, - Text, - Image, - TouchableOpacity,BackHandler + SafeAreaView, + StyleSheet, + ScrollView, + Linking, + View, + Text, + Image, + TouchableOpacity, + BackHandler, } from 'react-native'; -import colors from "../constants/colors"; +import colors from '../constants/colors'; import WebView from 'react-native-webview'; -import Button from "../components/Button"; -import backArrow from './../assets/images/backArrow.png' -import { - SearchAndImport -} from '../helpers/GoogleTakeOutAutoImport'; -import languages from './../locales/languages' +import Button from '../components/Button'; +import backArrow from './../assets/images/backArrow.png'; +import { SearchAndImport } from '../helpers/GoogleTakeOutAutoImport'; +import languages from './../locales/languages'; class ImportScreen extends Component { - constructor(props) { - super(props); + constructor(props) { + super(props); - // Autoimports if user has downloaded - SearchAndImport(); - } + // Autoimports if user has downloaded + SearchAndImport(); + } - backToMain() { - this.props.navigation.navigate('LocationTrackingScreen', {}) - } + backToMain() { + this.props.navigation.navigate('LocationTrackingScreen', {}); + } - handleBackPress = () => { - this.props.navigation.navigate('LocationTrackingScreen', {}); - return true; - }; + handleBackPress = () => { + this.props.navigation.navigate('LocationTrackingScreen', {}); + return true; + }; - componentDidMount() { - BackHandler.addEventListener("hardwareBackPress", this.handleBackPress); - } + componentDidMount() { + BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); + } - componentWillUnmount() { - BackHandler.removeEventListener("hardwareBackPress", this.handleBackPress); - } + componentWillUnmount() { + BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); + } - render() { - return ( - - - this.backToMain()}> - - - {languages.t('label.import_title')} - + render() { + return ( + + + this.backToMain()}> + + + + {languages.t('label.import_title')} + + - - - {languages.t('label.import_step_1')} - {languages.t('label.import_step_2')} - - - - - - - ) - } + + + + {languages.t('label.import_step_1')} + + + {languages.t('label.import_step_2')} + + + + + + + + ); + } } const styles = StyleSheet.create({ - // Container covers the entire screen - container: { - flex: 1, - flexDirection: 'column', - color: colors.PRIMARY_TEXT, - backgroundColor: colors.WHITE, - }, - headerTitle: { - textAlign: 'center', - fontWeight: "bold", - fontSize: 38, - padding: 0 - }, - subHeaderTitle: { - textAlign: 'center', - fontWeight: "bold", - fontSize: 22, - padding: 5 - }, - web: { - flex: 1, - width: "100%" - }, - main: { - flex: 1, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - paddingLeft: 20, - paddingRight: 20, - width: "100%" - }, - - headerContainer: { - flexDirection: 'row', - height:60, - borderBottomWidth:1, - borderBottomColor:'rgba(189, 195, 199,0.6)', - alignItems:'center' - }, - backArrowTouchable:{ - width:60, - height:60, - paddingTop:21, - paddingLeft:20 - }, - backArrow: { - height: 18, - width: 18.48 - }, - headerTitle:{ - fontSize: 24, - fontFamily:'OpenSans-Bold' - }, - sectionDescription: { - fontSize: 16, - lineHeight: 24, - textAlignVertical: 'center', - marginTop:12, - fontFamily:'OpenSans-Regular' - }, + // Container covers the entire screen + container: { + flex: 1, + flexDirection: 'column', + color: colors.PRIMARY_TEXT, + backgroundColor: colors.WHITE, + }, + subHeaderTitle: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 22, + padding: 5, + }, + web: { + flex: 1, + width: '100%', + }, + main: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + paddingLeft: 20, + paddingRight: 20, + width: '100%', + }, + headerContainer: { + flexDirection: 'row', + height: 60, + borderBottomWidth: 1, + borderBottomColor: 'rgba(189, 195, 199,0.6)', + alignItems: 'center', + }, + backArrowTouchable: { + width: 60, + height: 60, + paddingTop: 21, + paddingLeft: 20, + }, + backArrow: { + height: 18, + width: 18.48, + }, + headerTitle: { + fontSize: 24, + fontFamily: 'OpenSans-Bold', + }, + sectionDescription: { + fontSize: 16, + lineHeight: 24, + textAlignVertical: 'center', + marginTop: 12, + fontFamily: 'OpenSans-Regular', + }, }); -export default ImportScreen; \ No newline at end of file +export default ImportScreen; diff --git a/app/views/Licenses.js b/app/views/Licenses.js new file mode 100644 index 0000000000..3bb0545206 --- /dev/null +++ b/app/views/Licenses.js @@ -0,0 +1,147 @@ +import React, { Component } from 'react'; +import { + SafeAreaView, + StyleSheet, + View, + Text, + Image, + Dimensions, + TouchableOpacity, + BackHandler, +} from 'react-native'; + +import colors from '../constants/colors'; +import backArrow from './../assets/images/backArrow.png'; +import languages from './../locales/languages'; + +const width = Dimensions.get('window').width; + +class LicensesScreen extends Component { + constructor(props) { + super(props); + } + + backToMain() { + this.props.navigation.navigate('LocationTrackingScreen', {}); + } + + handleBackPress = () => { + this.props.navigation.navigate('LocationTrackingScreen', {}); + return true; + }; + + componentDidMount() { + BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); + } + + componentWillUnmount() { + BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); + } + + render() { + return ( + + + this.backToMain()}> + + + Licenses + + + + + {/* This screen is a placeholder for complete license content, or a link */} + {languages.t('label.license_placeholder')} + + + + ); + } +} + +const styles = StyleSheet.create({ + // Container covers the entire screen + container: { + flex: 1, + flexDirection: 'column', + color: colors.PRIMARY_TEXT, + backgroundColor: colors.WHITE, + }, + subHeaderTitle: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 22, + padding: 5, + }, + main: { + flex: 1, + flexDirection: 'column', + textAlignVertical: 'top', + // alignItems: 'center', + padding: 20, + width: '96%', + alignSelf: 'center', + }, + buttonTouchable: { + borderRadius: 12, + backgroundColor: '#665eff', + height: 52, + alignSelf: 'center', + width: width * 0.7866, + marginTop: 30, + justifyContent: 'center', + }, + buttonText: { + fontFamily: 'OpenSans-Bold', + fontSize: 14, + lineHeight: 19, + letterSpacing: 0, + textAlign: 'center', + color: '#ffffff', + }, + mainText: { + fontSize: 18, + lineHeight: 24, + fontWeight: '400', + textAlignVertical: 'center', + padding: 20, + }, + smallText: { + fontSize: 10, + lineHeight: 24, + fontWeight: '400', + textAlignVertical: 'center', + padding: 20, + }, + headerTitle: { + fontSize: 24, + fontFamily: 'OpenSans-Bold', + }, + headerContainer: { + flexDirection: 'row', + height: 60, + borderBottomWidth: 1, + borderBottomColor: 'rgba(189, 195, 199,0.6)', + alignItems: 'center', + }, + backArrowTouchable: { + width: 60, + height: 60, + paddingTop: 21, + paddingLeft: 20, + }, + backArrow: { + height: 18, + width: 18.48, + }, + sectionDescription: { + fontSize: 16, + lineHeight: 24, + marginTop: 12, + fontFamily: 'OpenSans-Regular', + }, +}); + +export default LicensesScreen; diff --git a/app/views/LocationTracking.js b/app/views/LocationTracking.js index f996012bd0..75d957d6d9 100644 --- a/app/views/LocationTracking.js +++ b/app/views/LocationTracking.js @@ -1,273 +1,405 @@ -import React, { - Component -} from 'react'; +import React, { Component } from 'react'; import { - SafeAreaView, - StyleSheet, - Linking, - View, - Text, - TouchableOpacity, - Dimensions, - Image, - ScrollView, - BackHandler, - Button + SafeAreaView, + StyleSheet, + Linking, + View, + Text, + TouchableOpacity, + Dimensions, + Image, + ScrollView, + BackHandler, } from 'react-native'; -import colors from "../constants/colors"; +import { + Menu, + MenuOptions, + MenuOption, + MenuTrigger, +} from 'react-native-popup-menu'; +import colors from '../constants/colors'; import LocationServices from '../services/LocationService'; +import BackgroundGeolocation from '@mauron85/react-native-background-geolocation'; import exportImage from './../assets/images/export.png'; import news from './../assets/images/newspaper.png'; - +import kebabIcon from './../assets/images/kebabIcon.png'; import pkLogo from './../assets/images/PKLogo.png'; -import {GetStoreData, SetStoreData} from '../helpers/General'; -import languages from './../locales/languages' +import { GetStoreData, SetStoreData } from '../helpers/General'; +import languages from './../locales/languages'; const width = Dimensions.get('window').width; class LocationTracking extends Component { - constructor(props) { - super(props); - - this.state = { - isLogging:'' + constructor(props) { + super(props); + + this.state = { + isLogging: '', + }; + } + + componentDidMount() { + BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); + GetStoreData('PARTICIPATE') + .then(isParticipating => { + console.log(isParticipating); + + if (isParticipating === 'true') { + this.setState({ + isLogging: true, + }); + this.willParticipate(); + } else { + this.setState({ + isLogging: false, + }); } - } + }) + .catch(error => console.log(error)); + } + componentWillUnmount() { + BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); + } - componentDidMount() { - BackHandler.addEventListener("hardwareBackPress", this.handleBackPress); - GetStoreData('PARTICIPATE') - .then(isParticipating => { - console.log(isParticipating); - - if(isParticipating === 'true'){ - this.setState({ - isLogging:true - }) - this.willParticipate() - } - else{ - this.setState({ - isLogging:false - }) - } - }) - .catch(error => console.log(error)) - } - componentWillUnmount() { BackHandler.removeEventListener("hardwareBackPress", this.handleBackPress); } + handleBackPress = () => { + BackHandler.exitApp(); // works best when the goBack is async + return true; + }; + export() { + this.props.navigation.navigate('ExportScreen', {}); + } - handleBackPress = () => { - BackHandler.exitApp(); // works best when the goBack is async - return true; - }; - export() { - this.props.navigation.navigate('ExportScreen', {}) - } + import() { + this.props.navigation.navigate('ImportScreen', {}); + } - import() { - this.props.navigation.navigate('ImportScreen', {}) + overlap() { + this.props.navigation.navigate('OverlapScreen', {}) } - news() { - this.props.navigation.navigate('NewsScreen', {}) - } + willParticipate = () => { + SetStoreData('PARTICIPATE', 'true').then(() => LocationServices.start()); - willParticipate =()=> { - SetStoreData('PARTICIPATE', 'true').then(() => - LocationServices.start() - ); + // Check and see if they actually authorized in the system dialog. + // If not, stop services and set the state to !isLogging + // Fixes tripleblindmarket/private-kit#129 + BackgroundGeolocation.checkStatus(({ authorization }) => { + if (authorization === BackgroundGeolocation.AUTHORIZED) { this.setState({ - isLogging:true - }) - } - - setOptOut =()=>{ - LocationServices.stop(this.props.navigation) + isLogging: true, + }); + } else if (authorization === BackgroundGeolocation.NOT_AUTHORIZED) { + LocationServices.stop(this.props.navigation); this.setState({ - isLogging:false - }) - } + isLogging: false, + }); + } + }); + }; - render() { - return ( - + news() { + this.props.navigation.navigate('NewsScreen', {}); + } - - - + licenses() { + this.props.navigation.navigate('LicensesScreen', {}); + } - {languages.t('label.private_kit')} + willParticipate = () => { + SetStoreData('PARTICIPATE', 'true').then(() => LocationServices.start()); + this.setState({ + isLogging: true, + }); + }; - { - this.state.isLogging ? ( - <> - + setOptOut = () => { + LocationServices.stop(this.props.navigation); + this.setState({ + isLogging: false, + }); + }; - this.setOptOut()} style={styles.stopLoggingButtonTouchable} > - {languages.t('label.stop_logging')} - - - ) : ( - <> - - this.willParticipate()} style={styles.startLoggingButtonTouchable} > - {languages.t('label.start_logging')} - - ) - } + render() { + return ( + + + + {/* A modal menu. Currently only used for license info */} + + + + + + { + this.licenses(); + }}> + Licenses + + + + + + {languages.t('label.private_kit')} + - - {this.state.isLogging ? - {languages.t('label.logging_message')} : - {languages.t('label.not_logging_message')} } - + {this.state.isLogging ? ( + <> + + this.setOptOut()} + style={styles.stopLoggingButtonTouchable}> + + {languages.t('label.stop_logging')} + + + this.overlap()} style={styles.startLoggingButtonTouchable} > + {languages.t('label.overlap')} + + + ) : ( + <> + + this.willParticipate()} + style={styles.startLoggingButtonTouchable}> + + {languages.t('label.start_logging')} + + + + )} - - + {this.state.isLogging ? ( + + {languages.t('label.logging_message')} + + ) : ( + + {languages.t('label.not_logging_message')} + + )} + + - - this.import()} style={styles.actionButtonsTouchable}> - - {languages.t('label.import')} - + + this.import()} + style={styles.actionButtonsTouchable}> + + + {languages.t('label.import')} + + - this.export()} style={styles.actionButtonsTouchable}> - - {languages.t('label.export')} - + this.export()} + style={styles.actionButtonsTouchable}> + + + {languages.t('label.export')} + + - this.news()} style={styles.actionButtonsTouchable}> - - {languages.t('label.news')} - - - + this.news()} + style={styles.actionButtonsTouchable}> + + + {languages.t('label.news')} + + + + - - {languages.t('label.url_info')} - Linking.openURL('https://privatekit.mit.edu')}>{languages.t('label.private_kit_url')} - - - ) - } + + + {languages.t('label.url_info')}{' '} + + Linking.openURL('https://privatekit.mit.edu')}> + {languages.t('label.private_kit_url')} + + + + ); + } } const styles = StyleSheet.create({ - // Container covers the entire screen - container: { - flex: 1, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - color: colors.PRIMARY_TEXT, - backgroundColor: colors.WHITE, - }, - headerTitle: { - textAlign: 'center', - fontSize: 38, - padding: 0, - fontFamily:'OpenSans-Bold' - }, - subHeaderTitle: { - textAlign: 'center', - fontWeight: "bold", - fontSize: 22, - padding: 5 - }, - main: { - flex: 1, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - width: "80%" - }, - block: { - margin: 20, - width: "100%" - }, - footer: { - textAlign: 'center', - fontSize: 12, - fontWeight: '600', - padding: 4, - paddingBottom: 10 - }, - intro: { - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'stretch', - }, - sectionDescription: { - fontSize: 12, - lineHeight: 24, - fontFamily:'OpenSans-Regular', - marginTop: 20, - marginLeft: 10, - marginRight: 10 - }, - startLoggingButtonTouchable:{ - borderRadius: 12, - backgroundColor: "#665eff", - height:52, - alignSelf:'center', - width:width*.7866, - marginTop:30, - justifyContent:'center' - }, - startLoggingButtonText:{ - fontFamily: "OpenSans-Bold", - fontSize: 14, - lineHeight: 19, - letterSpacing: 0, - textAlign: "center", - color: "#ffffff" - }, - stopLoggingButtonTouchable:{ - borderRadius: 12, - backgroundColor: "#fd4a4a", - height:52, - alignSelf:'center', - width:width*.7866, - marginTop:30, - justifyContent:'center', - }, - stopLoggingButtonText:{ - fontFamily: "OpenSans-Bold", - fontSize: 14, - lineHeight: 19, - letterSpacing: 0, - textAlign: "center", - color: "#ffffff" - }, - actionButtonsView:{ - width:width*.7866, - flexDirection:'row', - justifyContent:'space-between', - marginTop:64 - }, - actionButtonsTouchable:{ - height: 76, - borderRadius: 8, - backgroundColor: "#454f63", - width:width*.23, - justifyContent:'center', - alignItems:'center' - }, - actionButtonImage:{ - height:21.6, - width:32.2 - }, - actionButtonText:{ - opacity: 0.56, - fontFamily: "OpenSans-Bold", - fontSize: 12, - lineHeight: 17, - letterSpacing: 0, - textAlign: "center", - color: "#ffffff", - marginTop:6 - } + // Container covers the entire screen + container: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + color: colors.PRIMARY_TEXT, + backgroundColor: colors.WHITE, + }, + headerTitle: { + textAlign: 'center', + fontSize: 38, + padding: 0, + fontFamily: 'OpenSans-Bold', + }, + subHeaderTitle: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 22, + padding: 5, + }, + main: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '80%', + }, + block: { + margin: 20, + width: '100%', + }, + footer: { + textAlign: 'center', + fontSize: 12, + fontWeight: '600', + padding: 4, + paddingBottom: 10, + }, + intro: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'stretch', + }, + sectionDescription: { + fontSize: 12, + lineHeight: 24, + fontFamily: 'OpenSans-Regular', + marginTop: 20, + marginLeft: 10, + marginRight: 10, + }, + startLoggingButtonTouchable: { + borderRadius: 12, + backgroundColor: '#665eff', + height: 52, + alignSelf: 'center', + width: width * 0.7866, + marginTop: 30, + justifyContent: 'center', + }, + startLoggingButtonText: { + fontFamily: 'OpenSans-Bold', + fontSize: 14, + lineHeight: 19, + letterSpacing: 0, + textAlign: 'center', + color: '#ffffff', + }, + stopLoggingButtonTouchable: { + borderRadius: 12, + backgroundColor: '#fd4a4a', + height: 52, + alignSelf: 'center', + width: width * 0.7866, + marginTop: 30, + justifyContent: 'center', + }, + stopLoggingButtonText: { + fontFamily: 'OpenSans-Bold', + fontSize: 14, + lineHeight: 19, + letterSpacing: 0, + textAlign: 'center', + color: '#ffffff', + }, + actionButtonsView: { + width: width * 0.7866, + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 64, + }, + actionButtonsTouchable: { + height: 76, + borderRadius: 8, + backgroundColor: '#454f63', + width: width * 0.23, + justifyContent: 'center', + alignItems: 'center', + }, + actionButtonImage: { + height: 21.6, + width: 32.2, + }, + actionButtonText: { + opacity: 0.56, + fontFamily: 'OpenSans-Bold', + fontSize: 12, + lineHeight: 17, + letterSpacing: 0, + textAlign: 'center', + color: '#ffffff', + marginTop: 6, + }, + menuOptionText: { + fontFamily: 'OpenSans-Regular', + fontSize: 14, + padding: 10, + }, }); export default LocationTracking; diff --git a/app/views/News.js b/app/views/News.js index 3752d28f44..ff243de18d 100644 --- a/app/views/News.js +++ b/app/views/News.js @@ -1,124 +1,123 @@ -import React, { - Component -} from 'react'; +import React, { Component } from 'react'; import { - SafeAreaView, - StyleSheet, - ScrollView, - Image, - View, - Text, - TouchableOpacity,BackHandler + SafeAreaView, + StyleSheet, + Image, + View, + Text, + TouchableOpacity, + BackHandler, } from 'react-native'; -import colors from "../constants/colors"; -import { - WebView -} from 'react-native-webview'; -import Button from "../components/Button"; -import backArrow from './../assets/images/backArrow.png' -import languages from './../locales/languages' +import colors from '../constants/colors'; +import { WebView } from 'react-native-webview'; +import Button from '../components/Button'; +import backArrow from './../assets/images/backArrow.png'; +import languages from './../locales/languages'; class NewsScreen extends Component { - constructor(props) { - super(props); - } + constructor(props) { + super(props); + } + + backToMain() { + this.props.navigation.navigate('LocationTrackingScreen', {}); + } - backToMain() { - this.props.navigation.navigate('LocationTrackingScreen', {}) - } + handleBackPress = () => { + this.props.navigation.navigate('LocationTrackingScreen', {}); + return true; + }; - handleBackPress = () => { - this.props.navigation.navigate('LocationTrackingScreen', {}); - return true; - }; + componentDidMount() { + BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); + } - componentDidMount() { - BackHandler.addEventListener("hardwareBackPress", this.handleBackPress); - } + componentWillUnmount() { + BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); + } - componentWillUnmount() { - BackHandler.removeEventListener("hardwareBackPress", this.handleBackPress); - } + render() { + return ( + + + this.backToMain()}> + + + + {languages.t('label.latest_news')} + + - render() { - return ( - - - this.backToMain()}> - - - {languages.t('label.latest_news')} - - - - - ) - } + + + ); + } } const styles = StyleSheet.create({ - // Container covers the entire screen - container: { - flex: 1, - flexDirection: 'column', - color: colors.PRIMARY_TEXT, - backgroundColor: colors.WHITE, - }, - headerContainer: { - flexDirection: 'row', - }, - backArrow: { - fontSize: 60, - lineHeight: 60, - fontWeight: '400', - marginRight: 5, - textAlignVertical: 'center' - }, - sectionDescription: { - fontSize: 24, - lineHeight: 24, - fontWeight: '800', - textAlignVertical: 'center' - }, - web: { - flex: 1, - width: "100%", - margin: 0, - padding: 0 - }, - headerContainer: { - flexDirection: 'row', - height:60, - borderBottomWidth:1, - borderBottomColor:'rgba(189, 195, 199,0.6)', - alignItems:'center' - }, - backArrowTouchable:{ - width:60, - height:60, - paddingTop:21, - paddingLeft:20 - }, - backArrow: { - height: 18, - width: 18.48 - }, - headerTitle:{ - fontSize: 24, - fontFamily:'OpenSans-Bold', - }, - sectionDescription: { - fontSize: 16, - lineHeight: 24, - textAlignVertical: 'center', - marginTop:12, - fontFamily:'OpenSans-Regular' - } - + // Container covers the entire screen + container: { + flex: 1, + flexDirection: 'column', + color: colors.PRIMARY_TEXT, + backgroundColor: colors.WHITE, + }, + headerContainer: { + flexDirection: 'row', + }, + backArrow: { + fontSize: 60, + lineHeight: 60, + fontWeight: '400', + marginRight: 5, + textAlignVertical: 'center', + }, + sectionDescription: { + fontSize: 24, + lineHeight: 24, + fontWeight: '800', + textAlignVertical: 'center', + }, + web: { + flex: 1, + width: '100%', + margin: 0, + padding: 0, + }, + headerContainer: { + flexDirection: 'row', + height: 60, + borderBottomWidth: 1, + borderBottomColor: 'rgba(189, 195, 199,0.6)', + alignItems: 'center', + }, + backArrowTouchable: { + width: 60, + height: 60, + paddingTop: 21, + paddingLeft: 20, + }, + backArrow: { + height: 18, + width: 18.48, + }, + headerTitle: { + fontSize: 24, + fontFamily: 'OpenSans-Bold', + }, + sectionDescription: { + fontSize: 16, + lineHeight: 24, + textAlignVertical: 'center', + marginTop: 12, + fontFamily: 'OpenSans-Regular', + }, }); -export default NewsScreen; \ No newline at end of file +export default NewsScreen; diff --git a/app/views/Overlap.js b/app/views/Overlap.js new file mode 100644 index 0000000000..0bf687107c --- /dev/null +++ b/app/views/Overlap.js @@ -0,0 +1,407 @@ +import React, { + Component +} from 'react'; +import { + SafeAreaView, + StyleSheet, + ScrollView, + Linking, + View, + Text, + Image, + Dimensions, + TouchableOpacity,BackHandler +} from 'react-native'; + +import colors from "../constants/colors"; +import WebView from 'react-native-webview'; +import Button from "../components/Button"; +import { + GetStoreData +} from '../helpers/General'; +import { + convertPointsToString +} from '../helpers/convertPointsToString'; +import Share from 'react-native-share'; +import RNFetchBlob from 'rn-fetch-blob'; +import LocationServices from '../services/LocationService'; +import backArrow from './../assets/images/backArrow.png' +import languages from './../locales/languages' +import { Marker, PROVIDER_GOOGLE } from 'react-native-maps'; +import CustomCircle from '../helpers/customCircle'; +import MapView from 'react-native-map-clustering'; + + +const width = Dimensions.get('window').width; + +const base64 = RNFetchBlob.base64 + +const public_data = "https://docs.google.com/spreadsheets/d/1itaohdPiAeniCXNlntNztZ_oRvjh0HsGuJXUJWET008/export?format=csv" +const show_button_text = "Show Me Trace Overlap"; +const INITIAL_REGION = { + latitude: 36.56, + longitude: 20.39, + latitudeDelta: 50, + longitudeDelta: 50, +}; + +function distance (lat1, lon1, lat2, lon2) { + if ((lat1 == lat2) && (lon1 == lon2)) { + return 0; + } + else { + var radlat1 = Math.PI * lat1/180; + var radlat2 = Math.PI * lat2/180; + var theta = lon1 - lon2; + var radtheta = Math.PI * theta / 180; + var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta); + if (dist > 1) { + dist = 1; + } + dist = Math.acos(dist); + dist = dist * 180/Math.PI; + dist = dist * 60 * 1.1515; + return dist * 1.609344; + } +} + +class OverlapScreen extends Component { + constructor(props) { + super(props); + this.state = { + 'region': {}, + 'markers': [], + 'circles': [], + 'showButton': {'disabled': false, 'text': show_button_text}, + 'initialRegion': INITIAL_REGION, + }; + this.getInitialState(); + this.setMarkers(); + } + + getOverlap = async () => { + try { + + } catch (error) { + console.log(error.message); + } + }; + + setMarkers = async() => { + GetStoreData('LOCATION_DATA') + .then(locationArrayString => { + var locationArray = JSON.parse(locationArrayString); + if (locationArray === null) { + console.log(locationArray); + } + else { + var markers = []; + for (var i = 0; i < locationArray.length - 1; i+=1) { + console.log(i); + const coord = locationArray[i]; + const marker = { + coordinate: { + latitude: coord["latitude"], + longitude: coord["longitude"], + }, + key: i + 1, + color: '#f26964', + } + markers.push(marker); + } + this.setState({ + markers: markers + }); + } + }); + }; + + getInitialState = async () => { + try { + GetStoreData('LOCATION_DATA') + .then(locationArrayString => { + var locationArray = JSON.parse(locationArrayString); + if (locationArray === null) { + console.log(locationArray); + } + else { + var lastCoords = locationArray[locationArray.length - 1]; + this.setState({ + initialRegion: { + latitude: lastCoords["latitude"], + longitude: lastCoords["longitude"], + latitudeDelta: 10.10922, + longitudeDelta: 10.20421, + }, + markers: [{coordinate: { + latitude: lastCoords["latitude"], + longitude: lastCoords["longitude"], + }, + key: 0, + color: '#f26964', + },], + }); + } + }); + } catch (error) { + console.log(error); + } + }; + + downloadAndPlot = async () => { + // Downloads the file on the disk and loads it into memory + try { + this.setState({'showButton': {'disabled': true, + 'text': 'Loading Public Trace'}}); + RNFetchBlob + .config({ + // add this option that makes response data to be stored as a file, + // this is much more performant. + fileCache : true, + }) + .fetch('GET', public_data, { + //some headers .. + }) + .then((res) => { + // the temp file path + console.log('The file saved to ', res.path()) + try { + RNFetchBlob.fs.readFile(res.path(), 'utf8') + .then((records) => { + // delete the file first using flush + res.flush(); + this.parseCSV(records) + .then((parsedRecords) =>{ + console.log(parsedRecords); + console.log(Object.keys(parsedRecords).length); + this.plotCircles(parsedRecords) + .then(() => { + this.setState({'showButton': {'disabled': false, + 'text': show_button_text}}); + }); + }); + }) + .catch((e) => { + console.error("got error: ", e); + }) + } catch (err) { + console.log('ERROR:', err); + } + }) + } catch(e) { + console.log(e); + } + } + + parseCSV = async (records) => { + try { + var latestLat = this.state.initialRegion.latitude; + var latestLong = this.state.initialRegion.longitude; + const rows = records.split('\n'); + var parsedRows = {} + for (var i = rows.length - 1; i >= 0; i--) { + var row = rows[i].split(','); + const lat = parseFloat(row[7]); + const long = parseFloat(row[8]); + if (!isNaN(lat) && !isNaN(long)) { + // Threshold for using only points in 4k + // if (distance(latestLat, latestLong, lat, long) < 4000) { + if (true) { + key = String(lat) + "|" + String(long) + if (!(key in parsedRows)) { + parsedRows[key] = 0 + } + parsedRows[key] += 1 + } + } + } + return parsedRows; + } catch(e) { + console.log(e); + } + }; + + plotCircles = async (records) => { + try { + var circles = []; + for (const key in records) { + const latitude = parseFloat(key.split('|')[0]); + const longitude = parseFloat(key.split('|')[1]); + const count = records[key]; + if (!isNaN(latitude) && !isNaN(longitude)) { + var circle = { + center: { + latitude: latitude, + longitude: longitude, + }, + radius: 50 * count + } + circles.push(circle); + } + console.log(count); + } + this.setState({ + circles : circles + }); + console.log("done!"); + } catch(e) { + console.log(e); + } + }; + + backToMain() { + this.props.navigation.navigate('LocationTrackingScreen', {}) + } + + handleBackPress = () => { + this.props.navigation.navigate('LocationTrackingScreen', {}); + return true; + }; + + componentDidMount() { + BackHandler.addEventListener("hardwareBackPress", this.handleBackPress); + } + + componentWillUnmount() { + BackHandler.removeEventListener("hardwareBackPress", this.handleBackPress); + } + + render() { + return ( + + + + this.backToMain()}> + + + {languages.t('label.overlap')} + + + {this.state.markers.map(marker => ( + + ))} + {this.state.circles.map(circle => ( + + ))} + + + this.downloadAndPlot()} + disabled={this.state.showButton.disabled}> + {languages.t(this.state.showButton.text)} + + {languages.t('label.overlap_para_1')} + + + + ) + } +} + +const styles = StyleSheet.create({ + // Container covers the entire screen + container: { + flex: 1, + flexDirection: 'column', + color: colors.PRIMARY_TEXT, + backgroundColor: colors.WHITE + }, + headerTitle: { + textAlign: 'center', + fontWeight: "bold", + fontSize: 38, + + padding: 0 + }, + subHeaderTitle: { + textAlign: 'center', + fontWeight: "bold", + fontSize: 22, + padding: 5 + }, + main: { + flex: 1, + flexDirection: 'column', + textAlignVertical: 'top', + // alignItems: 'center', + padding:20, + width:'96%', + alignSelf:'center' + }, + buttonTouchable: { + borderRadius: 12, + backgroundColor: "#665eff", + height:52, + alignSelf:'center', + width:width*.7866, + marginTop:30, + justifyContent:'center' + }, + buttonText:{ + + fontFamily: "OpenSans-Bold", + fontSize: 14, + lineHeight: 19, + letterSpacing: 0, + textAlign: "center", + color: "#ffffff" + }, + mainText: { + fontSize: 18, + lineHeight: 24, + fontWeight: '400', + textAlignVertical: 'center', + padding: 20, + }, + smallText: { + fontSize: 10, + lineHeight: 24, + fontWeight: '400', + textAlignVertical: 'center', + padding: 20, + }, + + headerContainer: { + flexDirection: 'row', + height:60, + borderBottomWidth:1, + borderBottomColor:'rgba(189, 195, 199,0.6)', + alignItems:'center' + }, + backArrowTouchable:{ + width:60, + height:60, + paddingTop:21, + paddingLeft:20 + }, + backArrow: { + height: 18, + width: 18.48 + }, + headerTitle:{ + fontSize: 24, + fontFamily:'OpenSans-Bold' + }, + sectionDescription: { + fontSize: 16, + lineHeight: 24, + marginTop:12, + fontFamily:'OpenSans-Regular', + }, +}); + +export default OverlapScreen; \ No newline at end of file diff --git a/app/views/Welcome.js b/app/views/Welcome.js index c475b9f41c..dce653b0ab 100644 --- a/app/views/Welcome.js +++ b/app/views/Welcome.js @@ -1,125 +1,137 @@ import React, { Component } from 'react'; -import { - SafeAreaView, - StyleSheet, - ScrollView, - Linking, - View, - Text -} from 'react-native'; +import { SafeAreaView, StyleSheet, Linking, View, Text } from 'react-native'; -import colors from "../constants/colors"; -import Button from "../components/Button"; +import colors from '../constants/colors'; +import Button from '../components/Button'; -import {GetStoreData, SetStoreData} from '../helpers/General'; +import { GetStoreData, SetStoreData } from '../helpers/General'; class Welcome extends Component { - constructor(props) { - super(props); - } - componentDidMount() { - GetStoreData('PARTICIPATE') - .then(isParticipating => { - console.log(isParticipating); - if(isParticipating == 'true') { - this.props.navigation.navigate('LocationTrackingScreen', {}) - } - }) - .catch(error => console.log(error)) - } - - componentWillUnmount() { - } - - willParticipate() { - SetStoreData('PARTICIPATE', 'true').then(() => - this.props.navigation.navigate('LocationTrackingScreen', {}) - ); - } + constructor(props) { + super(props); + } + componentDidMount() { + GetStoreData('PARTICIPATE') + .then(isParticipating => { + console.log(isParticipating); + if (isParticipating == 'true') { + this.props.navigation.navigate('LocationTrackingScreen', {}); + } + }) + .catch(error => console.log(error)); + } - render() { - return ( - + componentWillUnmount() {} - - - + willParticipate() { + SetStoreData('PARTICIPATE', 'true').then(() => + this.props.navigation.navigate('LocationTrackingScreen', {}), + ); + } - Private Kit + render() { + return ( + + + + + Private Kit - Private Kit is your personal vault that nobody else can access. - It will allow you to log your location privately every five minutes. Your location information will NOT leave your phone. + + Private Kit is your personal vault that nobody else can access. + + + It will allow you to log your location privately every five + minutes. Your location information will NOT leave your phone. + + + - - + +