1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile: Resolves #720: Add "Sync only on Wi-Fi" option (#4729)

This commit is contained in:
mbalint 2021-03-29 10:35:39 +02:00 committed by GitHub
parent 17295734fd
commit c516ab405b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 156 additions and 5 deletions

View File

@ -121,6 +121,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/registry.d.ts
packages/app-cli/tests/registry.js
packages/app-cli/tests/registry.js.map
packages/app-cli/tests/services/plugins/RepositoryApi.d.ts packages/app-cli/tests/services/plugins/RepositoryApi.d.ts
packages/app-cli/tests/services/plugins/RepositoryApi.js packages/app-cli/tests/services/plugins/RepositoryApi.js
packages/app-cli/tests/services/plugins/RepositoryApi.js.map packages/app-cli/tests/services/plugins/RepositoryApi.js.map

3
.gitignore vendored
View File

@ -108,6 +108,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/registry.d.ts
packages/app-cli/tests/registry.js
packages/app-cli/tests/registry.js.map
packages/app-cli/tests/services/plugins/RepositoryApi.d.ts packages/app-cli/tests/services/plugins/RepositoryApi.d.ts
packages/app-cli/tests/services/plugins/RepositoryApi.js packages/app-cli/tests/services/plugins/RepositoryApi.js
packages/app-cli/tests/services/plugins/RepositoryApi.js.map packages/app-cli/tests/services/plugins/RepositoryApi.js.map

View File

@ -0,0 +1,84 @@
import Setting from '@joplin/lib/models/Setting';
import { reg } from '@joplin/lib/registry';
const sync = {
start: jest.fn().mockReturnValue({}),
};
describe('Registry', function() {
let originalSyncTarget: typeof reg.syncTarget;
beforeAll(() => {
Setting.setConstant('env', 'prod');
originalSyncTarget = reg.syncTarget;
reg.syncTarget = () => ({
isAuthenticated: () => true,
synchronizer: () => sync,
});
});
afterAll(() => {
Setting.setConstant('env', 'dev');
reg.syncTarget = originalSyncTarget;
});
beforeEach(() => {
jest.useFakeTimers();
Setting.setValue('sync.interval', 300);
});
afterEach(() => {
Setting.setValue('sync.interval', 0);
reg.setupRecurrentSync();
});
describe('when on mobile data', () => {
beforeEach(() => {
Setting.setValue('sync.mobileWifiOnly', true);
reg.setIsOnMobileData(true);
});
it('should not sync automatically', () => {
reg.setupRecurrentSync();
jest.runOnlyPendingTimers();
expect(sync.start).toHaveBeenCalledTimes(0);
});
it('should sync if do wifi check is false', done => {
void reg.scheduleSync(1, null, false)
.then(() =>{
expect(sync.start).toHaveBeenCalled();
done();
});
jest.runOnlyPendingTimers();
});
it('should sync if "sync only over wifi" is disabled in settings', () => {
Setting.setValue('sync.mobileWifiOnly', false);
reg.setupRecurrentSync();
jest.runOnlyPendingTimers();
expect(sync.start).toHaveBeenCalled();
});
});
describe('when not on mobile data', () => {
beforeEach(() => {
Setting.setValue('sync.mobileWifiOnly', true);
reg.setIsOnMobileData(false);
});
it('should sync automatically', () => {
reg.setupRecurrentSync();
jest.runOnlyPendingTimers();
expect(sync.start).toHaveBeenCalled();
});
});
});

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- RN-NOTIFICATION --> <!-- RN-NOTIFICATION -->
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />

View File

@ -345,6 +345,14 @@ class SideMenuContentComponent extends Component {
); );
} }
if (this.props.syncOnlyOverWifi && this.props.isOnMobileData) {
items.push(
<Text key="net_info" style={this.styles().syncStatus}>
{ _('Mobile data - auto-sync disabled') }
</Text>
);
}
return <View style={{ flex: 0, flexDirection: 'column', paddingBottom: theme.marginBottom }}>{items}</View>; return <View style={{ flex: 0, flexDirection: 'column', paddingBottom: theme.marginBottom }}>{items}</View>;
} }
@ -404,6 +412,8 @@ const SideMenuContent = connect(state => {
collapsedFolderIds: state.collapsedFolderIds, collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker, decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher, resourceFetcher: state.resourceFetcher,
isOnMobileData: state.isOnMobileData,
syncOnlyOverWifi: state.settings['sync.mobileWifiOnly'],
}; };
})(SideMenuContentComponent); })(SideMenuContentComponent);

View File

@ -18,6 +18,7 @@
"@react-native-community/clipboard": "^1.5.0", "@react-native-community/clipboard": "^1.5.0",
"@react-native-community/datetimepicker": "^3.0.3", "@react-native-community/datetimepicker": "^3.0.3",
"@react-native-community/geolocation": "^2.0.2", "@react-native-community/geolocation": "^2.0.2",
"@react-native-community/netinfo": "^6.0.0",
"@react-native-community/push-notification-ios": "^1.6.0", "@react-native-community/push-notification-ios": "^1.6.0",
"@react-native-community/slider": "^3.0.3", "@react-native-community/slider": "^3.0.3",
"buffer": "^5.0.8", "buffer": "^5.0.8",

View File

@ -29,6 +29,7 @@ import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar } = require('react-native'); const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar } = require('react-native');
import NetInfo from '@react-native-community/netinfo';
const DropdownAlert = require('react-native-dropdownalert').default; const DropdownAlert = require('react-native-dropdownalert').default;
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default; const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
const SafeAreaView = require('./components/SafeAreaView'); const SafeAreaView = require('./components/SafeAreaView');
@ -118,7 +119,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
if (action.type == 'NAV_GO') Keyboard.dismiss(); if (action.type == 'NAV_GO') Keyboard.dismiss();
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) { if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }); if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }, true);
SearchEngine.instance().scheduleSyncTables(); SearchEngine.instance().scheduleSyncTables();
} }
@ -151,7 +152,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
// Schedule a sync operation so that items that need to be encrypted // Schedule a sync operation so that items that need to be encrypted
// are sent to sync target. // are sent to sync target.
void reg.scheduleSync(); void reg.scheduleSync(null, null, true);
} }
if (action.type == 'NAV_GO' && action.routeName == 'Notes') { if (action.type == 'NAV_GO' && action.routeName == 'Notes') {
@ -194,6 +195,7 @@ const appDefaultState = Object.assign({}, defaultState, {
route: DEFAULT_ROUTE, route: DEFAULT_ROUTE,
noteSelectionEnabled: false, noteSelectionEnabled: false,
noteSideMenuOptions: null, noteSideMenuOptions: null,
isOnMobileData: false,
}); });
const appReducer = (state = appDefaultState, action: any) => { const appReducer = (state = appDefaultState, action: any) => {
@ -359,6 +361,12 @@ const appReducer = (state = appDefaultState, action: any) => {
newState.noteSideMenuOptions = action.options; newState.noteSideMenuOptions = action.options;
break; break;
case 'MOBILE_DATA_WARNING_UPDATE':
newState = Object.assign({}, state);
newState.isOnMobileData = action.isOnMobileData;
break;
} }
} catch (error) { } catch (error) {
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`; error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
@ -583,7 +591,9 @@ async function initialize(dispatch: Function) {
// When the app starts we want the full sync to // When the app starts we want the full sync to
// start almost immediately to get the latest data. // start almost immediately to get the latest data.
void reg.scheduleSync(1000).then(() => { // doWifiConnectionCheck set to true so initial sync
// doesn't happen on mobile data
void reg.scheduleSync(1000, null, true).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation // Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications. // might change the notifications.
void AlarmService.updateAllNotifications(); void AlarmService.updateAllNotifications();
@ -646,6 +656,22 @@ class AppComponent extends React.Component {
state: 'initializing', state: 'initializing',
}); });
try {
// This will be called right after adding the event listener
// so there's no need to check netinfo on startup
this.unsubscribeNetInfoHandler_ = NetInfo.addEventListener(({ type, details }) => {
const isMobile = details.isConnectionExpensive || type === 'cellular';
reg.setIsOnMobileData(isMobile);
this.props.dispatch({
type: 'MOBILE_DATA_WARNING_UPDATE',
isOnMobileData: isMobile,
});
});
} catch (error) {
reg.logger().warn('Something went wrong while checking network info');
reg.logger().info(error);
}
await initialize(this.props.dispatch); await initialize(this.props.dispatch);
this.props.dispatch({ this.props.dispatch({
@ -679,6 +705,7 @@ class AppComponent extends React.Component {
componentWillUnmount() { componentWillUnmount() {
AppState.removeEventListener('change', this.onAppStateChange_); AppState.removeEventListener('change', this.onAppStateChange_);
if (this.unsubscribeNetInfoHandler_) this.unsubscribeNetInfoHandler_();
} }
componentDidUpdate(prevProps: any) { componentDidUpdate(prevProps: any) {

View File

@ -949,6 +949,15 @@ class Setting extends BaseModel {
}, },
storage: SettingStorage.File, storage: SettingStorage.File,
}, },
'sync.mobileWifiOnly': {
value: false,
type: SettingItemType.Bool,
section: 'sync',
public: true,
label: () => _('Synchronise only over WiFi connection'),
storage: SettingStorage.File,
appTypes: ['mobile'],
},
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, public: false, appTypes: ['desktop'] }, noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, public: false, appTypes: ['desktop'] },
tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: ['desktop'] }, tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: ['desktop'] },
folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: ['desktop'] }, folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: ['desktop'] },

View File

@ -15,6 +15,7 @@ class Registry {
private scheduleSyncId_: any; private scheduleSyncId_: any;
private recurrentSyncId_: any; private recurrentSyncId_: any;
private db_: any; private db_: any;
private isOnMobileData_ = false;
logger() { logger() {
if (!this.logger_) { if (!this.logger_) {
@ -38,6 +39,12 @@ class Registry {
this.showErrorMessageBoxHandler_(message); this.showErrorMessageBoxHandler_(message);
} }
// If isOnMobileData is true, the doWifiConnectionCheck is not set
// and the sync.mobileWifiOnly setting is true it will cancel the sync.
setIsOnMobileData(isOnMobileData: boolean) {
this.isOnMobileData_ = isOnMobileData;
}
resetSyncTarget(syncTargetId: number = null) { resetSyncTarget(syncTargetId: number = null) {
if (syncTargetId === null) syncTargetId = Setting.value('sync.target'); if (syncTargetId === null) syncTargetId = Setting.value('sync.target');
delete this.syncTargets_[syncTargetId]; delete this.syncTargets_[syncTargetId];
@ -74,7 +81,7 @@ class Registry {
} }
}; };
scheduleSync = async (delay: number = null, syncOptions: any = null) => { scheduleSync = async (delay: number = null, syncOptions: any = null, doWifiConnectionCheck: boolean = false) => {
this.schedSyncCalls_.push(true); this.schedSyncCalls_.push(true);
try { try {
@ -104,6 +111,12 @@ class Registry {
this.scheduleSyncId_ = null; this.scheduleSyncId_ = null;
this.logger().info('Preparing scheduled sync'); this.logger().info('Preparing scheduled sync');
if (doWifiConnectionCheck && Setting.value('sync.mobileWifiOnly') && this.isOnMobileData_) {
this.logger().info('Sync cancelled because we\'re on mobile data');
promiseResolve();
return;
}
const syncTargetId = Setting.value('sync.target'); const syncTargetId = Setting.value('sync.target');
if (!(await this.syncTarget(syncTargetId).isAuthenticated())) { if (!(await this.syncTarget(syncTargetId).isAuthenticated())) {
@ -191,7 +204,7 @@ class Registry {
this.recurrentSyncId_ = shim.setInterval(() => { this.recurrentSyncId_ = shim.setInterval(() => {
this.logger().info('Running background sync on timer...'); this.logger().info('Running background sync on timer...');
void this.scheduleSync(0); void this.scheduleSync(0, null, true);
}, 1000 * Setting.value('sync.interval')); }, 1000 * Setting.value('sync.interval'));
} }
} finally { } finally {