Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ set(MM_SRCS
streamingintervaltype.cpp
synchronizationerror.cpp
synchronizationmanager.cpp
usagereportcontroller.cpp
valuerelationcontroller.cpp
valuerelationfeaturesmodel.cpp
variablesmanager.cpp
Expand Down Expand Up @@ -190,6 +191,7 @@ set(MM_HDRS
synchronizationerror.h
synchronizationmanager.h
synchronizationoptions.h
usagereportcontroller.h
valuerelationcontroller.h
valuerelationfeaturesmodel.h
variablesmanager.h
Expand Down
2 changes: 2 additions & 0 deletions app/androidutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ void AndroidUtils::handleActivityResult( const int receiverRequestCode, const in
QJniObject::fromString( mTargetPath ).object<jstring>() )
.toString();
emit imageSelected( newUri, mLastCode );
emit photoFromGallery();
}
else if ( receiverRequestCode == CAMERA_CODE && resultCode == RESULT_OK )
{
Expand All @@ -394,6 +395,7 @@ void AndroidUtils::handleActivityResult( const int receiverRequestCode, const in
const QString absolutePath = absolutePathJNI.toString();

emit imageSelected( absolutePath, mLastCode );
emit photoCaptured();
}
else
{
Expand Down
2 changes: 2 additions & 0 deletions app/androidutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class AndroidUtils: public QObject

signals:
void imageSelected( QString imagePath, QString code );
void photoCaptured();
void photoFromGallery();
void bluetoothEnabled( bool state );
void notifyInfo( const QString &msg );
void notifyError( const QString &msg );
Expand Down
76 changes: 75 additions & 1 deletion app/appsettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ const QString AppSettings::POSITION_PROVIDERS_GROUP = QStringLiteral( "inputApp/

AppSettings::AppSettings( QObject *parent ): QObject( parent )
{
// Usage report settings live outside the app group
{
QSettings settings;
mUsageReportEnabled = settings.value( QStringLiteral( "usage_report/enabled" ), false ).toBool();
mUsageReportConsentAsked = settings.value( QStringLiteral( "usage_report/consent_asked" ), false ).toBool();
}

QSettings settings;
settings.beginGroup( CoreUtils::QSETTINGS_APP_GROUP_NAME );
const QString path = settings.value( QStringLiteral( "defaultProject" ), "" ).toString();
Expand Down Expand Up @@ -391,4 +398,71 @@ void AppSettings::setWindowPosition( const QList<QVariant> &newWindowPosition )
setValue( QStringLiteral( "windowPosition" ), QVariant::fromValue( newWindowPosition ) );

emit windowPositionChanged();
}
}

bool AppSettings::usageReportEnabled() const
{
return mUsageReportEnabled;
}

void AppSettings::setUsageReportEnabled( bool enabled )
{
if ( mUsageReportEnabled == enabled )
return;

mUsageReportEnabled = enabled;

QSettings settings;
if ( !mUsageReportEnabled )
{
// Opt-out: clear accumulated data but preserve enabled and consent_asked flags
settings.beginGroup( QStringLiteral( "usage_report/data" ) );
settings.remove( QString() );
settings.endGroup();
settings.setValue( QStringLiteral( "usage_report/enabled" ), false );
}
else
{
settings.setValue( QStringLiteral( "usage_report/enabled" ), true );
}

emit usageReportEnabledChanged( mUsageReportEnabled );
}

bool AppSettings::usageReportConsentAsked() const
{
return mUsageReportConsentAsked;
}

void AppSettings::setUsageReportConsentAsked( bool asked )
{
if ( mUsageReportConsentAsked == asked )
return;

mUsageReportConsentAsked = asked;

QSettings settings;
settings.setValue( QStringLiteral( "usage_report/consent_asked" ), asked );

emit usageReportConsentAskedChanged( mUsageReportConsentAsked );
}

void AppSettings::trackUsageFeature( const QString &key )
{
if ( !mUsageReportEnabled ) return;
QSettings().setValue( QStringLiteral( "usage_report/data/" ) + key, true );
}

void AppSettings::incrementUsageCounter( const QString &key )
{
if ( !mUsageReportEnabled ) return;
QSettings s;
const QString fullKey = QStringLiteral( "usage_report/data/" ) + key;
s.setValue( fullKey, s.value( fullKey, 0 ).toInt() + 1 );
}

// Temporary: request an immediate usage snapshot for testing
void AppSettings::requestImmediateUsageSnapshot()
{
emit immediateUsageSnapshotRequested();
}
22 changes: 22 additions & 0 deletions app/appsettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class AppSettings: public QObject
Q_PROPERTY( QList<QVariant> windowPosition READ windowPosition WRITE setWindowPosition NOTIFY windowPositionChanged )
Q_PROPERTY( HapticsType hapticsType READ hapticsType WRITE setHapticsType NOTIFY hapticsTypeChanged )
Q_PROPERTY( StartupBehavior startupBehavior READ startupBehavior WRITE setStartupBehavior NOTIFY startupBehaviorChanged )
Q_PROPERTY( bool usageReportEnabled READ usageReportEnabled WRITE setUsageReportEnabled NOTIFY usageReportEnabledChanged )
Q_PROPERTY( bool usageReportConsentAsked READ usageReportConsentAsked WRITE setUsageReportConsentAsked NOTIFY usageReportConsentAskedChanged )

public:
// enum of haptic modes we support
Expand Down Expand Up @@ -122,6 +124,18 @@ class AppSettings: public QObject
StartupBehavior startupBehavior() const;
void setStartupBehavior( StartupBehavior startupBehavior );

bool usageReportEnabled() const;
void setUsageReportEnabled( bool enabled );

bool usageReportConsentAsked() const;
void setUsageReportConsentAsked( bool asked );

Q_INVOKABLE void trackUsageFeature( const QString &key );
Q_INVOKABLE void incrementUsageCounter( const QString &key );

// Temporary: request an immediate usage snapshot (bypasses interval check)
Q_INVOKABLE void requestImmediateUsageSnapshot();

public slots:
void setReuseLastEnteredValues( bool reuseLastEnteredValues );

Expand All @@ -147,6 +161,12 @@ class AppSettings: public QObject

void windowPositionChanged();

void usageReportEnabledChanged( bool enabled );
void usageReportConsentAskedChanged( bool asked );

// Temporary: emitted when QML requests an immediate snapshot
void immediateUsageSnapshotRequested();

private:
// Projects path
QString mDefaultProject;
Expand Down Expand Up @@ -183,6 +203,8 @@ class AppSettings: public QObject

HapticsType mHapticsType;
StartupBehavior mStartupBehavior;
bool mUsageReportEnabled = false;
bool mUsageReportConsentAsked = false;
};

#endif // APPSETTINGS_H
8 changes: 8 additions & 0 deletions app/ios/iosutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ IosUtils::IosUtils( QObject *parent ): QObject( parent )
QObject::connect( mImagePicker, &IOSImagePicker::imageCaptured, this, [this]( const QString & absoluteImagePath )
{
emit imageSelected( absoluteImagePath, mLastCode );
if ( mLastCode.isEmpty() )
return;
if ( mLastSourceWasCamera )
emit photoCaptured();
else
emit photoFromGallery();
} );
QObject::connect( mImagePicker, &IOSImagePicker::notifyError, this, &IosUtils::notifyError );
}
Expand All @@ -34,12 +40,14 @@ bool IosUtils::isIos() const
void IosUtils::callImagePicker( const QString &targetPath, const QString &code )
{
mLastCode = code;
mLastSourceWasCamera = false;
mImagePicker->showImagePicker( targetPath );
}

void IosUtils::callCamera( const QString &targetPath, const QString &code )
{
mLastCode = code;
mLastSourceWasCamera = true;
mImagePicker->callCamera( targetPath, mPositionKit, mCompass );
}

Expand Down
3 changes: 3 additions & 0 deletions app/ios/iosutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class IosUtils: public QObject

signals:
void imageSelected( const QString &imagePath, const QString &code );
void photoCaptured();
void photoFromGallery();
void notifyError( const QString &message );
void positionKitChanged();
void compassChanged();
Expand All @@ -61,6 +63,7 @@ class IosUtils: public QObject
Compass *mCompass = nullptr;

QString mLastCode;
bool mLastSourceWasCamera = false;
/**
* Calls the objective-c function to disable idle timer to prevent screen from sleeping.
*/
Expand Down
109 changes: 108 additions & 1 deletion app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include <QFontDatabase>
#include <QGuiApplication>
#include <QSettings>
#include <QQmlApplicationEngine>
#include <QQmlComponent>
#include <QtDebug>
Expand Down Expand Up @@ -103,6 +104,9 @@
#include "mixedattributevalue.h"
#include "photosketchingcontroller.h"
#include "mapsketchingcontroller.h"
#include "filter/filtercontroller.h"
#include "autosynccontroller.h"
#include "usagereportcontroller.h"

#include "projectsmodel.h"
#include "projectsproxymodel.h"
Expand Down Expand Up @@ -529,6 +533,7 @@ int main( int argc, char *argv[] )
vm->registerInputExpressionFunctions();

SynchronizationManager syncManager( ma.get() );
UsageReportController usageReportController( as, &localProjectsManager, ma.get() );

LayerTreeModelPixmapProvider *layerTreeModelPixmapProvider( new LayerTreeModelPixmapProvider );
LayerTreeFlatModelPixmapProvider *layerTreeFlatModelPixmapProvider( new LayerTreeFlatModelPixmapProvider );
Expand Down Expand Up @@ -640,9 +645,108 @@ int main( int argc, char *argv[] )
syncManager.syncProject( project, SyncOptions::Authorized, SyncOptions::Retry, requestOrigin );
} );

QObject::connect( &activeProject, &ActiveProject::projectReloaded, &lambdaContext, [merginApi = ma.get(), &activeProject]()
// Temporary: immediate snapshot trigger from QML test button
QObject::connect( as, &AppSettings::immediateUsageSnapshotRequested, &usageReportController, &UsageReportController::forceSubmitSnapshot );

// Gather dynamic usage data via signal connections
UsageReportController &urc = usageReportController;

// Auto-sync
QObject::connect( &activeProject, &ActiveProject::autosyncControllerChanged, &lambdaContext, [&urc]( AutosyncController * ac )
{
if ( !ac ) return;
QObject::connect( ac, &AutosyncController::projectSyncRequested, ac, [&urc]( SyncOptions::RequestOrigin origin )
{
if ( origin == SyncOptions::AutomaticRequest )
urc.trackFeature( QStringLiteral( "autosync" ) );
} );
} );

// Created project
QObject::connect( &pw, &ProjectWizard::projectCreated, &lambdaContext, [&urc]( const QString &, const QString & )
{
urc.trackFeature( QStringLiteral( "created_project" ) );
} );

// Photo captured vs attached
QObject::connect( &androidUtils, &AndroidUtils::photoCaptured, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "captured_images" ) );
} );
QObject::connect( &androidUtils, &AndroidUtils::photoFromGallery, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "attached_images" ) );
} );
QObject::connect( &iosUtils, &IosUtils::photoCaptured, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "captured_images" ) );
} );
QObject::connect( &iosUtils, &IosUtils::photoFromGallery, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "attached_images" ) );
} );

// Workspace switches
QObject::connect( ma->userInfo(), &MerginUserInfo::activeWorkspaceChanged, &lambdaContext, [&urc]()
{
urc.incrementCounter( QStringLiteral( "workspace_switches" ) );
} );

// External GPS provider
QObject::connect( pk, &PositionKit::positionProviderChanged, &lambdaContext, [&urc]( AbstractPositionProvider * provider )
{
if ( !provider ) return;
if ( provider->type() == QLatin1String( "internal" ) ) return;

urc.trackFeature( QStringLiteral( "external_gps" ) );

const QString id = provider->id();
QString connectionType;
if ( id == QLatin1String( "simulated" ) )
connectionType = QStringLiteral( "mock" );
else if ( id.contains( QLatin1Char( ':' ) ) )
connectionType = QStringLiteral( "bluetooth" );
else
connectionType = QStringLiteral( "network" );

urc.setData( QStringLiteral( "external_connection_type" ), connectionType );
urc.setData( QStringLiteral( "external_name" ), provider->name() );
} );

// Highest project role
QObject::connect( &activeProject, &ActiveProject::projectRoleChanged, &lambdaContext, [&urc, as, &activeProject]()
{
if ( !as->usageReportEnabled() ) return;

const QString role = activeProject.projectRole();
auto roleRank = []( const QString & r ) -> int
{
if ( r == QLatin1String( "owner" ) ) return 5;
if ( r == QLatin1String( "admin" ) ) return 4;
if ( r == QLatin1String( "writer" ) ) return 3;
if ( r == QLatin1String( "reader" ) ) return 2;
if ( r == QLatin1String( "guest" ) ) return 1;
return 0;
};

QSettings s;
const QString current = s.value( QStringLiteral( "usage_report/data/highest_role" ) ).toString();
if ( roleRank( role ) > roleRank( current ) )
urc.setData( QStringLiteral( "highest_role" ), role );
} );

// Project load time
QObject::connect( &activeProject, &ActiveProject::loadingStarted, &lambdaContext, [&urc]()
{
urc.startLoadTimer();
} );

urc.startPingTimer();

QObject::connect( &activeProject, &ActiveProject::projectReloaded, &lambdaContext, [merginApi = ma.get(), &activeProject, &urc]()
{
merginApi->reloadProjectRole( activeProject.projectFullName() );
urc.recordLoadTime();
} );

QObject::connect( ma.get(), &MerginApi::authChanged, &lambdaContext, [merginApi = ma.get(), &activeProject]()
Expand Down Expand Up @@ -831,6 +935,9 @@ int main( int argc, char *argv[] )
QQmlComponent component( &engine, QUrl( "qrc:/com.merginmaps/imports/MMInput/main.qml" ) );
QObject *object = component.create();

// Attempt weekly snapshot
usageReportController.trySubmitSnapshot();

if ( !component.errors().isEmpty() )
{
qDebug( "%s", QgsApplication::showSettings().toLocal8Bit().data() );
Expand Down
2 changes: 2 additions & 0 deletions app/mapsketchingcontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ void MapSketchingController::finishDigitizing()
mLayer->addFeature( feature );
mLayer->endEditCommand();
}

emit sketched();
}

void MapSketchingController::redo() const
Expand Down
1 change: 1 addition & 0 deletions app/mapsketchingcontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class MapSketchingController : public QObject
Q_INVOKABLE QStringList availableColors() const;

signals:
void sketched();
void highlightGeometryChanged();
void activeColorChanged();
void mapSettingsChanged();
Expand Down
1 change: 1 addition & 0 deletions app/qml/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ set(MM_QML
dialogs/MMPositionTrackingDialog.qml
dialogs/MMProjectLimitDialog.qml
dialogs/MMRemoveProjectDialog.qml
dialogs/MMUsageReportDialog.qml
dialogs/MMWelcomeToNewDesignDialog.qml
dialogs/MMSplittingFailedDialog.qml
dialogs/MMStorageLimitDialog.qml
Expand Down
Loading
Loading