Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Fix) Improve Google Location Data import Experience, fix import logic #410

Merged
merged 32 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
da2192c
initial commit. working on android
rparet Mar 24, 2020
c674afd
[WIP] Replace Web View Import with File Picker
rparet Mar 27, 2020
f3c3ae6
commit local WIP instead of stashing
rparet Mar 29, 2020
04a2528
working and tested on iOS and Android
rparet Mar 29, 2020
3ac4904
Rebase
rparet Mar 29, 2020
e46cc9c
Remove console warnings
rparet Mar 29, 2020
477c589
Jest mocks and snapshots
rparet Mar 30, 2020
d2bdf7b
Import Linking library
tremblerz Apr 1, 2020
3d8ca32
- Add logic to look for last 2 months of history locations
Apr 9, 2020
6c99619
Add unit tests for Google Import feature
Apr 10, 2020
7ebaaa7
Add unit tests for Google Import feature
Apr 10, 2020
9257174
Merge branch 'develop' into fix/google-import
tstirrat Apr 10, 2020
549f214
Remove empty diff
tstirrat Apr 10, 2020
1d45aee
Update yarn.lock
tstirrat Apr 10, 2020
21e9c33
Add more unit tests for Import screen
Apr 10, 2020
169f1e4
Merge branch 'develop' into fix/google-import
tstirrat Apr 10, 2020
e84eea8
Fix up lint error
tstirrat Apr 10, 2020
f12d74f
Merge branch 'develop' of https://github.com/tripleblindmarket/privat…
Apr 10, 2020
e9db50f
Merge branch 'fix/google-import' of github.com:sergesemashko/private-…
Apr 10, 2020
d3c2c03
Regenerate jetificableGroups.json
Apr 10, 2020
fb2a39c
Revert build.gradle
Apr 10, 2020
bf77e6a
Re-word "Google Drive" to "Add to Drive"
Apr 10, 2020
66f37ef
Merge branch 'develop' of https://github.com/tripleblindmarket/privat…
Apr 11, 2020
8f148ab
Apply hack around missing uri in 'react-native-fs'.readFile()
Apr 12, 2020
eb71376
Merge branch 'develop' of https://github.com/tripleblindmarket/privat…
Apr 12, 2020
287741f
Fix Import unit tests
Apr 12, 2020
0172052
Merge branch 'develop' into fix/google-import
sergesemashko Apr 12, 2020
cb8009e
Merge branch 'develop' into fix/google-import
tstirrat Apr 12, 2020
12dab19
Merge branch 'develop' of https://github.com/tripleblindmarket/privat…
Apr 15, 2020
5a43110
Address PR comments, add WiFi data note to instructions
Apr 15, 2020
dbed8b8
Merge branch 'develop' of https://github.com/tripleblindmarket/privat…
Apr 20, 2020
e47f98d
Apply the fix for the last 2 months January edge case
Apr 20, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions __mocks__/react-native-document-picker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
pick: jest.fn(),
pickMultiple: jest.fn(),
isCancel: jest.fn(),
};
19 changes: 18 additions & 1 deletion app/helpers/General.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AsyncStorage from '@react-native-community/async-storage';
// import _ from 'lodash';
import DocumentPicker from 'react-native-document-picker';

/**
* Get Data from Store
Expand Down Expand Up @@ -43,3 +43,20 @@ export async function SetStoreData(key, item) {
console.log(error.message);
}
}

export async function pickFile() {
// Pick a single file - returns actual path on Android, file:// uri on iOS
try {
const res = await DocumentPicker.pick({
type: [DocumentPicker.types.zip, DocumentPicker.types.allFiles],
usePath: true,
});
return res.uri;
} catch (err) {
if (DocumentPicker.isCancel(err)) {
// User cancelled the picker, exit any dialogs or menus and move on
} else {
throw err;
}
}
}
104 changes: 55 additions & 49 deletions app/helpers/GoogleData.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,73 @@ import { LOCATION_DATA } from '../constants/storage';
*/
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,
});
/**
* Rounds float number to a desired number of decimal places and returns a float
* number. NOTE: .toFixed() returns a string, but number is required.
* @param num - number
* @param digits - amount of digits to round
* @returns {number}
*/
function toFixedNumber(num, digits) {
tstirrat marked this conversation as resolved.
Show resolved Hide resolved
const pow = Math.pow(10, digits);
return Math.round(num * pow) / pow;
}

function LocationExists(localDataJSON, loc) {
let wasImportedBefore = false;
/**
* Formats a provided google placeVisit to a local format making sure
* float numbers have constant number of decimal places as float numbers
* has to be exact for later comparison.
*
* @param placeVisit - google place object
* @returns {{latitude: number, time: string, longitude: number}}
*/
function formatLocation(placeVisit) {
return {
latitude: toFixedNumber(placeVisit.location.latitudeE7 * 10 ** -7, 7),
longitude: toFixedNumber(placeVisit.location.longitudeE7 * 10 ** -7, 7),
time: placeVisit.duration.startTimestampMs,
};
}

for (let index = 0; index < localDataJSON.length; ++index) {
let storedLoc = localDataJSON[index];
function hasLocation(localDataJSON, loc) {
for (const storedLoc of localDataJSON) {
if (
storedLoc.latitude == loc.latitude &&
storedLoc.longitude == loc.longitude &&
storedLoc.time == loc.time
storedLoc.latitude === loc.latitude &&
storedLoc.longitude === loc.longitude &&
storedLoc.time === loc.time
) {
wasImportedBefore = true;
break;
return true;
}
}

return wasImportedBefore;
return false;
}

function InsertIfNew(localDataJSON, 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) {
let loc = BuildLocalFormat(data.placeVisit);
InsertIfNew(localDataJSON, loc);
}
});
function extractNewLocations(storedLocations, googleLocationHistory) {
sergesemashko marked this conversation as resolved.
Show resolved Hide resolved
return (googleLocationHistory?.timelineObjects || []).reduce(
(newLocations, location) => {
// Only import visited places, not paths for now
if (location?.placeVisit) {
const formattedLoc = formatLocation(location.placeVisit);
if (!hasLocation(storedLocations, formattedLoc)) {
newLocations.push(formattedLoc);
}
}
return newLocations;
},
[],
);
}

export async function MergeJSONWithLocalData(googleDataJSON) {
GetStoreData(LOCATION_DATA).then(locationArray => {
let locationData;

if (locationArray !== null) {
locationData = JSON.parse(locationArray);
} else {
locationData = [];
}
export async function mergeJSONWithLocalData(googleLocationHistory) {
let storedLocations = await GetStoreData(LOCATION_DATA, false);
storedLocations = Array.isArray(storedLocations) ? storedLocations : [];
const newLocations = extractNewLocations(
storedLocations,
googleLocationHistory,
);

Merge(locationData, googleDataJSON);
await SetStoreData(LOCATION_DATA, [...storedLocations, ...newLocations]);

console.log('Saving on array');
SetStoreData(LOCATION_DATA, locationData);
});
return newLocations;
}
206 changes: 119 additions & 87 deletions app/helpers/GoogleTakeOutAutoImport.js
Original file line number Diff line number Diff line change
@@ -1,109 +1,141 @@
import dayjs from 'dayjs';
import { Platform } from 'react-native';
import RNFS from 'react-native-fs';
/**
* Checks the download folder, unzips and imports all data from Google TakeOut
*/
import { subscribe, unzip } from 'react-native-zip-archive';

import { MergeJSONWithLocalData } from '../helpers/GoogleData';
import { mergeJSONWithLocalData } from '../helpers/GoogleData';
export class NoRecentLocationsError extends Error {}
export class InvalidFileExtensionError extends Error {}

// require the module
let RNFS = require('react-native-fs');

// unzipping progress component.
const ZIP_EXT_CHECK_REGEX = /\.zip$/;
let progress;
const MONTHS = [
'JANUARY',
'FEBRUARY',
'MARCH',
'APRIL',
'MAY',
'JUNE',
'JULY',
'AUGUST',
'SEPTEMBER',
'OCTOBER',
'NOVEMBER',
'DECEMBER',
];
/**
* Safe paths is interested in locations for latest a couple of weeks.
* Date for latest 2 months should be sufficient to cover all cases,
* especially the case when we are in the early days of the current month.
* @returns {string[]} - array of files for latest 2 months from google takeout archive
*/
export function getFilenamesForLatest2Months(rootPath, now) {
const previousMonth = dayjs(now).subtract(1, 'month');

// Google Takout File Format.
let takeoutZip = /^takeout[\w,\s-]+\.zip$/gm;

// 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 year = new Date().getFullYear();
// let month = monthNames[new Date().getMonth()].toUpperCase();
return (
RNFS.DownloadDirectoryPath +
'/Takeout/Location History/Semantic Location History/' +
year +
'/' +
year +
'_MARCH.json'
);
return [previousMonth, now].map(date => {
const year = date.year();
const monthStr = MONTHS[date.month()];
return (
`${rootPath}/Takeout/Location History/Semantic Location History/${year}/` +
`${year}_${monthStr}.json`
);
});
}

export async function SearchAndImport() {
//googleLocationJSON
console.log('Auto-import start');
// Imports any Takeout location data
// Currently works for Google Takeout Location data
export async function importTakeoutData(filePath) {
let unifiedPath = filePath;

if (Platform.OS === 'ios') {
unifiedPath = filePath.replace('file://', '');
}

if (!ZIP_EXT_CHECK_REGEX.test(unifiedPath)) {
throw new InvalidFileExtensionError();
}

// UnZip Progress Bar Log.
// progress callback is required by unzip().
progress = subscribe(
({
progress,
// filePath
// unifiedPath
}) => {
if (Math.trunc(progress * 100) % 10 === 0)
console.log('Unzipping', Math.trunc(progress * 100), '%');
console.log('[INFO] Unzipping', Math.trunc(progress * 100), '%');
},
);

// TODO: RNFS.DownloadDirectoryPath is not defined on iOS.
// Find out how to access Downloads folder.
if (!RNFS.DownloadDirectoryPath) {
return;
}
const extractDir = `${
RNFS.CachesDirectoryPath
}/Takeout-${new Date().toISOString()}`;

console.log('[INFO] Takeout import start. Path:', unifiedPath);

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,
);

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();
})
.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();
});
let newLocations = [];
let path;
let parsedFilesCount = 0;
try {
path = await unzip(unifiedPath, extractDir);

console.log(`[INFO] Unzip Completed for ${path}`);

const monthlyLocationFiles = getFilenamesForLatest2Months(path, dayjs());
for (const filepath of monthlyLocationFiles) {
console.log('[INFO] File to import:', filepath);

const isExist = await RNFS.exists(`file://${filepath}`);
if (isExist) {
console.log('[INFO] File exists:', `file://${filepath}`);

const contents = await RNFS.readFile(`file://${filepath}`).catch(
err => {
console.log(
`[INFO] Caught error on opening "file://${filepath}"`,
err,
);
console.log(
`[INFO] Attempting to open file "file://${filepath}" again`,
err,
);

/**
* IMPORTANT!!!
* A temporary hack around URI generation bug in react-native-fs on android.
* An exception is thrown as `file://` is not in the file URI:
* "Error: ENOENT: No content provider:
* /data/user/0/edu.mit.privatekit/cache/Takeout-2020-04-12T15:48:54.295Z/Takeout/Location History/Semantic Location History/2020/2020_APRIL.json,
* open '/data/user/0/edu.mit.privatekit/cache/Takeout-2020-04-12T15:48:54.295Z/Takeout/Location History/Semantic Location History/2020/2020_APRIL.json'
* "
* @see https://github.com/itinance/react-native-fs/blob/master/android/src/main/java/com/rnfs/RNFSManager.java#L110
*/
return RNFS.readFile(`file://file://${filepath}`);
},
);

newLocations = [
...newLocations,
...(await mergeJSONWithLocalData(JSON.parse(contents))),
];

console.log('[INFO] Imported file:', filepath);
parsedFilesCount++;
}
}
} catch (err) {
console.log('[Error] Failed to import Google Takeout', err);
}
// clean up unzipped folders
if (path) {
await RNFS.unlink(path);
}
progress.remove();
if (parsedFilesCount === 0) {
throw new NoRecentLocationsError();
}
return newLocations;
}
Loading