diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index aed98e357..38ecdb5f4 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -50,6 +50,9 @@ dependencies { // Nordpol compile 'com.fidesmo:nordpol-android:0.1.22' + // piwik + implementation 'org.piwik.sdk:piwik-sdk:3.0.3' + // libs as submodules implementation project(':libkeychain') implementation project(':openpgp-api-lib') diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 4d30d115d..317d9f5cd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -28,6 +28,9 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd; import java.io.File; import java.net.Proxy; +import java.util.Arrays; +import java.util.List; + public final class Constants { @@ -158,6 +161,10 @@ public final class Constants { public static final String KEY_SIGNATURES_TABLE_INITIALIZED = "key_signatures_table_initialized"; + public static final String KEY_ANALYTICS_ASKED_POLITELY = "analytics_asked"; + public static final String KEY_ANALYTICS_CONSENT = "analytics_consent"; + public static final String KEY_ANALYTICS_LAST_ASKED = "analytics_last_asked"; + public static final class Theme { public static final String LIGHT = "light"; public static final String DARK = "dark"; @@ -168,6 +175,14 @@ public final class Constants { public static final String TYPE_HTTP = "proxyHttp"; public static final String TYPE_SOCKS = "proxySocks"; } + + // we generally only track booleans. never snoop around in the user's string settings!! + public static final List ANALYTICS_PREFS = Arrays.asList(USE_NORMAL_PROXY, USE_TOR_PROXY, THEME, + SYNC_CONTACTS, SYNC_KEYSERVER, ENABLE_WIFI_SYNC_ONLY, EXPERIMENTAL_ENABLE_KEYBASE, + EXPERIMENTAL_ENABLE_LINKED_IDENTITIES, EXPERIMENTAL_USB_ALLOW_UNTESTED, + PASSPHRASE_CACHE_SUBS, SEARCH_KEYSERVER, SEARCH_KEYBASE, SEARCH_WEB_KEY_DIRECTORY, + TEXT_USE_COMPRESSION, TEXT_SELF_ENCRYPT, FILE_USE_COMPRESSION, FILE_SELF_ENCRYPT, USE_ARMOR, + USE_NUMKEYPAD_FOR_SECURITY_TOKEN_PIN, ENCRYPT_FILENAMES); } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java index b51071e3e..c8149ba76 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java @@ -30,6 +30,7 @@ import android.support.annotation.Nullable; import android.widget.Toast; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.sufficientlysecure.keychain.analytics.AnalyticsManager; import org.sufficientlysecure.keychain.network.TlsCertificatePinning; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; @@ -41,6 +42,7 @@ import timber.log.Timber.DebugTree; public class KeychainApplication extends Application { + AnalyticsManager analyticsManager; /** * Called when the application is starting, before any activity, service, or receiver objects @@ -105,6 +107,9 @@ public class KeychainApplication extends Application { KeyserverSyncManager.updateKeyserverSyncScheduleAsync(this, Constants.DEBUG_KEYSERVER_SYNC); TemporaryFileProvider.scheduleCleanupImmediately(); + + analyticsManager = AnalyticsManager.getInstance(getApplicationContext()); + analyticsManager.initialize(this); } /** @@ -152,4 +157,8 @@ public class KeychainApplication extends Application { Timber.plant(new DebugTree()); } } + + public AnalyticsManager getAnalyticsManager() { + return analyticsManager; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/analytics/AnalyticsConsentRequester.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/analytics/AnalyticsConsentRequester.java new file mode 100644 index 000000000..acf643ba6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/analytics/AnalyticsConsentRequester.java @@ -0,0 +1,97 @@ +package org.sufficientlysecure.keychain.analytics; + + +import java.util.concurrent.TimeUnit; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.preference.PreferenceActivity; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.BuildConfig; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.KeychainApplication; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.SettingsActivity; +import org.sufficientlysecure.keychain.ui.SettingsActivity.ExperimentalPrefsFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.util.Preferences; + + +public class AnalyticsConsentRequester { + private final Activity activity; + + public static AnalyticsConsentRequester getInstance(Activity activity) { + return new AnalyticsConsentRequester(activity); + } + + private AnalyticsConsentRequester(Activity activity) { + this.activity = activity; + } + + public void maybeAskForAnalytics() { + Preferences preferences = Preferences.getPreferences(activity); + if (preferences.isAnalyticsHasConsent()) { + return; + } + + boolean askedBeforeAndWasRejected = + preferences.isAnalyticsAskedPolitely() && !preferences.isAnalyticsHasConsent(); + if (!Constants.DEBUG && askedBeforeAndWasRejected) { + return; + } + + try { + long firstInstallTime = + activity.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, 0).firstInstallTime; + long threeDaysAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(3); + boolean installedLessThanThreeDaysAgo = firstInstallTime > threeDaysAgo; + if (!Constants.DEBUG && installedLessThanThreeDaysAgo) { + return; + } + } catch (NameNotFoundException e) { + return; + } + + long twentyFourHoursAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); + boolean askedLessThan24HoursAgo = preferences.getAnalyticsLastAsked() > twentyFourHoursAgo; + if (!Constants.DEBUG && askedLessThan24HoursAgo) { + return; + } + + preferences.setAnalyticsLastAskedNow(); + + AnalyticsManager analyticsManager = ((KeychainApplication) activity.getApplication()).getAnalyticsManager(); + AlertDialog alertDialog = new Builder(activity) + .setMessage(R.string.dialog_analytics_consent) + .setPositiveButton(R.string.button_analytics_yes, (dialog, which) -> { + preferences.setAnalyticsAskedPolitely(); + preferences.setAnalyticsGotUserConsent(true); + analyticsManager.refreshSettings(activity); + Notify.create(activity, R.string.snack_analytics_accept, Style.OK, + this::startExperimentalSettingsActivity, R.string.snackbutton_analytics_settings).show(); + }) + .setNegativeButton(R.string.button_analytics_no, (dialog, which) -> { + preferences.setAnalyticsAskedPolitely(); + preferences.setAnalyticsGotUserConsent(false); + analyticsManager.refreshSettings(activity); + Notify.create(activity, R.string.snack_analytics_reject, Style.OK, + this::startExperimentalSettingsActivity, R.string.snackbutton_analytics_settings).show(); + }) + .show(); + alertDialog.findViewById(android.R.id.message).setMovementMethod(LinkMovementMethod.getInstance()); + alertDialog.setCanceledOnTouchOutside(false); + } + + private void startExperimentalSettingsActivity() { + Intent resultIntent = new Intent(activity, SettingsActivity.class); + String experimentalPrefsName = ExperimentalPrefsFragment.class.getName(); + resultIntent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, experimentalPrefsName); + activity.startActivity(resultIntent); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/analytics/AnalyticsManager.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/analytics/AnalyticsManager.java new file mode 100644 index 000000000..bb3574d56 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/analytics/AnalyticsManager.java @@ -0,0 +1,158 @@ +package org.sufficientlysecure.keychain.analytics; + + +import android.app.Activity; +import android.app.Application; +import android.app.Application.ActivityLifecycleCallbacks; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Bundle; + +import org.piwik.sdk.Piwik; +import org.piwik.sdk.Tracker; +import org.piwik.sdk.TrackerConfig; +import org.piwik.sdk.extra.DownloadTracker.Extra.ApkChecksum; +import org.piwik.sdk.extra.TrackHelper; +import org.sufficientlysecure.keychain.Constants.Defaults; +import org.sufficientlysecure.keychain.Constants.Pref; +import org.sufficientlysecure.keychain.util.Preferences; +import timber.log.Timber; + + +public class AnalyticsManager implements OnSharedPreferenceChangeListener { + private Tracker piwikTracker; + + public static AnalyticsManager getInstance(Context context) { + return new AnalyticsManager(context); + } + + private AnalyticsManager(Context context) { + refreshSettings(context); + } + + public void initialize(Application application) { + if (piwikTracker != null) { + TrackHelper.track().download().identifier(new ApkChecksum(application)).with(piwikTracker); + } + + application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + } + + @Override + public void onActivityStarted(Activity activity) { + + } + + @Override + public void onActivityResumed(Activity activity) { + if (piwikTracker == null) { + return; + } + TrackHelper.track().screen(activity.getClass().getSimpleName()).with(piwikTracker); + } + + @Override + public void onActivityPaused(Activity activity) { + + } + + @Override + public void onActivityStopped(Activity activity) { + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + + } + + @Override + public void onActivityDestroyed(Activity activity) { + + } + }); + + Preferences preferences = Preferences.getPreferences(application); + preferences.getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + // we generally only track booleans. never snoop around in the user's string settings!! + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + // small exception: check if the user uses a custom keyserver, or one of the well-known ones + if (Pref.KEY_SERVERS.equals(key)) { + Timber.d("Tracking pref %s", key); + String keyServers = sharedPreferences.getString(Pref.KEY_SERVERS, Defaults.KEY_SERVERS); + String current = keyServers.substring(keyServers.indexOf(',')); + + String coarseGranularityKeyserver; + if (current.contains("keyserver.ubuntu.com")) { + coarseGranularityKeyserver = "ubuntu"; + } else if (current.contains("pgp.mit.edu")) { + coarseGranularityKeyserver = "mit"; + } else if (current.contains("pool.sks-keyservers.net")) { + coarseGranularityKeyserver = "pool"; + } else { + coarseGranularityKeyserver = "custom"; + } + TrackHelper.track().interaction("pref_" + Pref.KEY_SERVERS, coarseGranularityKeyserver).with(piwikTracker); + return; + } + if (Pref.ANALYTICS_PREFS.contains(key)) { + Timber.d("Tracking pref %s", key); + if (!sharedPreferences.contains(key)) { + TrackHelper.track().interaction("pref_" + key, "empty").with(piwikTracker); + return; + } + boolean value = sharedPreferences.getBoolean(key, false); + TrackHelper.track().interaction("pref_" + key, value ? "true" : "false").with(piwikTracker); + } + } + + public void trackFragmentImpression(String opClassName, String fragmentName) { + if (piwikTracker == null) { + return; + } + + TrackHelper.track().screen(opClassName + "/" + fragmentName).with(piwikTracker); + } + + public void trackInternalServiceCall(String opClassName) { + if (piwikTracker == null) { + return; + } + TrackHelper.track() + .interaction("internalApiCall", opClassName) + .with(piwikTracker); + } + + public void trackApiServiceCall(String opClassName, String currentCallingPackage) { + if (piwikTracker == null) { + return; + } + + TrackHelper.track() + .interaction("externalApiCall", opClassName) + .piece(currentCallingPackage.replace(".", "/")) + .with(piwikTracker); + } + + public synchronized void refreshSettings(Context context) { + boolean analyticsHasConsent = Preferences.getPreferences(context).isAnalyticsHasConsent(); + boolean analyticsEnabled = piwikTracker != null; + if (analyticsHasConsent != analyticsEnabled) { + if (analyticsHasConsent) { + TrackerConfig trackerConfig = new TrackerConfig("https://piwik.openkeychain.org/", 2, "OpenKeychain"); + piwikTracker = Piwik.getInstance(context).newTracker(trackerConfig); + piwikTracker.setDispatchInterval(60000); + piwikTracker.setOptOut(false); + } else { + piwikTracker.setOptOut(true); + piwikTracker = null; + } + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index 39e405a7b..b0e6eab6a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -53,6 +53,8 @@ import org.openintents.openpgp.OpenPgpSignatureResult.AutocryptPeerResult; import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.KeychainApplication; +import org.sufficientlysecure.keychain.analytics.AnalyticsManager; import org.sufficientlysecure.keychain.operations.BackupOperation; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.ExportResult; @@ -99,6 +101,7 @@ public class OpenPgpService extends Service { private ApiAppDao mApiAppDao; private OpenPgpServiceKeyIdExtractor mKeyIdExtractor; private ApiPendingIntentFactory mApiPendingIntentFactory; + private AnalyticsManager analyticsManager; @Override public void onCreate() { @@ -108,6 +111,8 @@ public class OpenPgpService extends Service { mApiPermissionHelper = new ApiPermissionHelper(this, mApiAppDao); mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext()); mKeyIdExtractor = OpenPgpServiceKeyIdExtractor.getInstance(getContentResolver(), mApiPendingIntentFactory); + + analyticsManager = ((KeychainApplication) getApplication()).getAnalyticsManager(); } private Intent signImpl(Intent data, InputStream inputStream, @@ -1025,6 +1030,8 @@ public class OpenPgpService extends Service { return errorResult; } + analyticsManager.trackApiServiceCall(data.getAction(), mApiPermissionHelper.getCurrentCallingPackage()); + Progressable progressable = null; if (data.hasExtra(OpenPgpApi.EXTRA_PROGRESS_MESSENGER)) { Messenger messenger = data.getParcelableExtra(OpenPgpApi.EXTRA_PROGRESS_MESSENGER); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainServiceTask.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainServiceTask.java index 305a566a1..2065dc1cc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainServiceTask.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainServiceTask.java @@ -21,11 +21,14 @@ package org.sufficientlysecure.keychain.service; import java.util.concurrent.atomic.AtomicBoolean; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.os.AsyncTask; import android.os.Parcelable; import android.support.v4.os.CancellationSignal; +import org.sufficientlysecure.keychain.KeychainApplication; +import org.sufficientlysecure.keychain.analytics.AnalyticsManager; import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.operations.BackupOperation; import org.sufficientlysecure.keychain.operations.BaseOperation; @@ -52,13 +55,20 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; public class KeychainServiceTask { - public static KeychainServiceTask create(Context context) { - return new KeychainServiceTask(context.getApplicationContext()); + private final AnalyticsManager analyticsManager; + + public static KeychainServiceTask create(Activity activity) { + Context context = activity.getApplicationContext(); + KeyWritableRepository keyRepository = KeyWritableRepository.create(context); + AnalyticsManager analyticsManager = ((KeychainApplication) activity.getApplication()).getAnalyticsManager(); + + return new KeychainServiceTask(context, keyRepository, analyticsManager); } - private KeychainServiceTask(Context context) { + private KeychainServiceTask(Context context, KeyWritableRepository keyRepository, AnalyticsManager analyticsManager) { this.context = context; - this.keyRepository = KeyWritableRepository.create(context); + this.keyRepository = keyRepository; + this.analyticsManager = analyticsManager; } private final Context context; @@ -121,6 +131,8 @@ public class KeychainServiceTask { return null; } + analyticsManager.trackInternalServiceCall(op.getClass().getSimpleName()); + // noinspection unchecked, we make sure it's the correct op above return op.execute(inputParcel, cryptoInput); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 82a93632e..32ce010d3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -54,6 +54,7 @@ import eu.davidea.flexibleadapter.SelectableAdapter.Mode; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.KeychainDatabase; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.analytics.AnalyticsConsentRequester; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager; import org.sufficientlysecure.keychain.daos.KeyRepository; @@ -259,6 +260,8 @@ public class KeyListFragment extends RecyclerFragment> liveData = viewModel.getGenericLiveData(requireContext(), this::loadFlexibleKeyItems); liveData.observe(this, this::onLoadKeyItems); + + AnalyticsConsentRequester.getInstance(activity).maybeAskForAnalytics(); } @WorkerThread diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index 9471c644d..358d9aafa 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -23,7 +23,6 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentManager.OnBackStackChangedListener; import android.support.v4.app.FragmentTransaction; import android.support.v7.widget.Toolbar; import android.view.View; @@ -37,7 +36,9 @@ import com.mikepenz.materialdrawer.model.DividerDrawerItem; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; +import org.sufficientlysecure.keychain.KeychainApplication; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.analytics.AnalyticsManager; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.remote.ui.AppsListFragment; import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity; @@ -62,6 +63,7 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai public Drawer mDrawer; private Toolbar mToolbar; + private AnalyticsManager analyticsManager; @Override public void onCreate(Bundle savedInstanceState) { @@ -72,6 +74,8 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai mToolbar.setTitle(R.string.app_name); setSupportActionBar(mToolbar); + analyticsManager = ((KeychainApplication) getApplication()).getAnalyticsManager(); + mDrawer = new DrawerBuilder() .withActivity(this) .withHeader(R.layout.main_drawer_header) @@ -200,6 +204,8 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai private void setFragment(Fragment frag) { FragmentManager fragmentManager = getSupportFragmentManager(); + analyticsManager.trackFragmentImpression(getClass().getSimpleName(), frag.getClass().getSimpleName()); + FragmentTransaction ft = fragmentManager.beginTransaction(); ft.replace(R.id.main_fragment_container, frag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java index f12943f14..09994ebb0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java @@ -584,6 +584,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity { }); } + @Override + public void onPause() { + super.onPause(); + + Activity activity = getActivity(); + ((KeychainApplication) activity.getApplication()).getAnalyticsManager().refreshSettings(activity); + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java index 71eb650c7..457d02354 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java @@ -42,11 +42,13 @@ import android.view.ViewPropertyAnimator; import android.view.animation.OvershootInterpolator; import com.astuetz.PagerSlidingTabStrip; +import org.sufficientlysecure.keychain.KeychainApplication; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.livedata.GenericLiveData; import org.sufficientlysecure.keychain.model.SubKey; import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.model.UserPacket.UserId; +import org.sufficientlysecure.keychain.analytics.AnalyticsManager; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.ui.adapter.PagerTabStripAdapter; @@ -62,12 +64,13 @@ public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeList KeyRepository keyRepository; // view - private ViewPager mViewPager; - private PagerSlidingTabStrip mSlidingTabLayout; + private ViewPager viewPager; + private PagerSlidingTabStrip slidingTabLayout; - private ActionMode mActionMode; + private ActionMode actionMode; private boolean hasSecret; - private boolean mActionIconShown; + private boolean actionIconShown; + private PagerTabStripAdapter tabAdapter; enum ViewKeyAdvTab { START(ViewKeyAdvStartFragment.class, R.string.key_view_tab_start, false), @@ -86,6 +89,8 @@ public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeList } } + private AnalyticsManager analyticsManager; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -93,9 +98,10 @@ public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeList setFullScreenDialogClose(v -> finish()); keyRepository = KeyRepository.create(this); + analyticsManager = ((KeychainApplication) getApplication()).getAnalyticsManager(); - mViewPager = findViewById(R.id.pager); - mSlidingTabLayout = findViewById(R.id.sliding_tab_layout); + viewPager = findViewById(R.id.pager); + slidingTabLayout = findViewById(R.id.sliding_tab_layout); if (!getIntent().hasExtra(EXTRA_MASTER_KEY_ID)) { throw new IllegalArgumentException("Missing required extra master_key_id"); @@ -185,7 +191,7 @@ public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeList } mToolbar.setBackgroundColor(color); mStatusBar.setBackgroundColor(ViewKeyActivity.getStatusBarBackgroundColor(color)); - mSlidingTabLayout.setBackgroundColor(color); + slidingTabLayout.setBackgroundColor(color); invalidateOptionsMenu(); } @@ -196,21 +202,21 @@ public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeList } private void initTabs() { - PagerTabStripAdapter tabAdapter = new PagerTabStripAdapter(this); - mViewPager.setAdapter(tabAdapter); + tabAdapter = new PagerTabStripAdapter(this); + viewPager.setAdapter(tabAdapter); for (ViewKeyAdvTab tab : ViewKeyAdvTab.values()) { tabAdapter.addTab(tab.fragmentClass, null, getString(tab.titleRes)); } // update layout after operations - mSlidingTabLayout.setViewPager(mViewPager); - mSlidingTabLayout.setOnPageChangeListener(this); + slidingTabLayout.setViewPager(viewPager); + slidingTabLayout.setOnPageChangeListener(this); // switch to tab selected by extra Intent intent = getIntent(); int switchToTab = intent.getIntExtra(EXTRA_SELECTED_TAB, 0); - mViewPager.setCurrentItem(switchToTab); + viewPager.setCurrentItem(switchToTab); } @Override @@ -234,15 +240,15 @@ public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeList getMenuInflater().inflate(R.menu.action_mode_edit, menu); final MenuItem vActionModeItem = menu.findItem(R.id.menu_action_mode_edit); - boolean isCurrentActionFragment = ViewKeyAdvTab.values()[mViewPager.getCurrentItem()].hasActionMode; + boolean isCurrentActionFragment = ViewKeyAdvTab.values()[viewPager.getCurrentItem()].hasActionMode; // if the state is as it should be, never mind - if (isCurrentActionFragment == mActionIconShown) { + if (isCurrentActionFragment == actionIconShown) { return isCurrentActionFragment; } // show or hide accordingly - mActionIconShown = isCurrentActionFragment; + actionIconShown = isCurrentActionFragment; vActionModeItem.setEnabled(isCurrentActionFragment); animateMenuItem(vActionModeItem, isCurrentActionFragment); @@ -273,13 +279,13 @@ public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeList @Override public void onActionModeStarted(final ActionMode mode) { super.onActionModeStarted(mode); - mActionMode = mode; + actionMode = mode; } @Override public void onActionModeFinished(ActionMode mode) { super.onActionModeFinished(mode); - mActionMode = null; + actionMode = null; } @Override @@ -289,11 +295,14 @@ public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeList @Override public void onPageSelected(int position) { - if (mActionMode != null) { - mActionMode.finish(); - mActionMode = null; + if (actionMode != null) { + actionMode.finish(); + actionMode = null; } invalidateOptionsMenu(); + + String fragmentName = tabAdapter.getItem(position).getClass().getSimpleName(); + analyticsManager.trackFragmentImpression(getClass().getSimpleName(), fragmentName); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java index 38773c116..790f0e845 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -33,6 +33,7 @@ import android.support.annotation.Nullable; import com.google.auto.value.AutoValue; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants.Pref; +import org.sufficientlysecure.keychain.analytics.AnalyticsManager; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; import timber.log.Timber; @@ -81,6 +82,10 @@ public class Preferences { mSharedPreferences = context.getSharedPreferences(PREF_FILE_NAME, PREF_FILE_MODE); } + public SharedPreferences getSharedPreferences() { + return mSharedPreferences; + } + public String getLanguage() { return mSharedPreferences.getString(Constants.Pref.LANGUAGE, ""); } @@ -353,6 +358,30 @@ public class Preferences { mSharedPreferences.edit().putBoolean(Pref.SYNC_IS_SCHEDULED, isScheduled).apply(); } + public boolean isAnalyticsAskedPolitely() { + return mSharedPreferences.getBoolean(Pref.KEY_ANALYTICS_ASKED_POLITELY, false); + } + + public void setAnalyticsAskedPolitely() { + mSharedPreferences.edit().putBoolean(Pref.KEY_ANALYTICS_ASKED_POLITELY, true).apply(); + } + + public boolean isAnalyticsHasConsent() { + return mSharedPreferences.getBoolean(Pref.KEY_ANALYTICS_CONSENT, false); + } + + public void setAnalyticsGotUserConsent(boolean hasUserConsent) { + mSharedPreferences.edit().putBoolean(Pref.KEY_ANALYTICS_CONSENT, hasUserConsent).apply(); + } + + public void setAnalyticsLastAskedNow() { + mSharedPreferences.edit().putLong(Pref.KEY_ANALYTICS_LAST_ASKED, System.currentTimeMillis()).apply(); + } + + public long getAnalyticsLastAsked() { + return mSharedPreferences.getLong(Pref.KEY_ANALYTICS_LAST_ASKED, 0); + } + @AutoValue public static abstract class CloudSearchPrefs implements Parcelable { public abstract boolean isKeyserverEnabled(); diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 5a7d2ac92..5e457eded 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -236,6 +236,9 @@ "Contact keybase.io for key proofs and show them every time a key is displayed" "(The icons and many screens are not yet adjusted accordingly for the dark theme)" + Allow anonymous usage statistics + If enabled, sends anonymous usage statistics to help improve the app + "Enable Tor" "Requires Orbot to be installed" @@ -2045,4 +2048,11 @@ Anonymous # + "To improve the experience for all users, may OpenKeychain collect anonymous usage statistics?\n\nTo find out more, see our Privacy Policy." + "Yes, I want to help!" + "No, thanks" + + "Thanks for helping out! You can change this preference in the settings." + "That's alright, we won't ask again. You can change your mind in the settings." + "Settings" diff --git a/OpenKeychain/src/main/res/xml/experimental_preferences.xml b/OpenKeychain/src/main/res/xml/experimental_preferences.xml index e557f06e6..fd21a546c 100644 --- a/OpenKeychain/src/main/res/xml/experimental_preferences.xml +++ b/OpenKeychain/src/main/res/xml/experimental_preferences.xml @@ -5,6 +5,13 @@ android:summary="@string/label_experimental_settings_desc_summary" android:title="@string/label_experimental_settings_desc_title" /> + +