diff --git a/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java b/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java index 7c50c642..6a4b0ff4 100644 --- a/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java @@ -1,12 +1,15 @@ package com.nutomic.syncthingandroid.activities; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.ComponentName; import android.content.DialogInterface; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; @@ -20,6 +23,7 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar.Tab; import android.support.v7.app.ActionBar.TabListener; import android.support.v7.app.ActionBarDrawerToggle; +import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -31,15 +35,28 @@ import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.fragments.DevicesFragment; import com.nutomic.syncthingandroid.fragments.DrawerFragment; import com.nutomic.syncthingandroid.fragments.FoldersFragment; +import com.nutomic.syncthingandroid.syncthing.RestApi; import com.nutomic.syncthingandroid.syncthing.SyncthingService; +import java.util.Date; + /** - * Shows {@link com.nutomic.syncthingandroid.fragments.FoldersFragment} and {@link com.nutomic.syncthingandroid.fragments.DevicesFragment} in different tabs, and + * Shows {@link com.nutomic.syncthingandroid.fragments.FoldersFragment} and + * {@link com.nutomic.syncthingandroid.fragments.DevicesFragment} in different tabs, and * {@link com.nutomic.syncthingandroid.fragments.DrawerFragment} in the navigation drawer. */ public class MainActivity extends SyncthingActivity implements SyncthingService.OnApiChangeListener { + private static final String TAG = "MainActivity"; + + /** + * Time after first start when usage reporting dialog should be shown. + * + * @see #showUsageReportingDialog() + */ + private static final long USAGE_REPORTING_DIALOG_DELAY = 3 * 24 * 60 * 60 * 1000; + private AlertDialog mLoadingDialog; private AlertDialog mDisabledDialog; @@ -49,64 +66,88 @@ public class MainActivity extends SyncthingActivity */ @Override @SuppressLint("InflateParams") - public void onApiChange(final SyncthingService.State currentState) { - runOnUiThread(new Runnable() { - @Override - public void run() { - if (currentState != SyncthingService.State.ACTIVE && !isFinishing()) { - if (currentState == SyncthingService.State.DISABLED) { - if (mLoadingDialog != null) { - mLoadingDialog.dismiss(); - mLoadingDialog = null; - } - mDisabledDialog = SyncthingService.showDisabledDialog(MainActivity.this); - } else if (mLoadingDialog == null) { - final SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(MainActivity.this); - - LayoutInflater inflater = getLayoutInflater(); - View dialogLayout = inflater.inflate(R.layout.loading_dialog, null); - TextView loadingText = (TextView) dialogLayout.findViewById(R.id.loading_text); - loadingText.setText((getService().isFirstStart()) - ? R.string.web_gui_creating_key - : R.string.api_loading); - - mLoadingDialog = new AlertDialog.Builder(MainActivity.this) - .setCancelable(false) - .setView(dialogLayout) - .show(); - - // Make sure the first start dialog is shown on top. - if (prefs.getBoolean("first_start", true)) { - new AlertDialog.Builder(MainActivity.this) - .setTitle(R.string.welcome_title) - .setMessage(R.string.welcome_text) - .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - prefs.edit().putBoolean("first_start", false).commit(); - } - }) - .show(); - } - } - return; - } - + public void onApiChange(SyncthingService.State currentState) { + if (currentState == SyncthingService.State.ACTIVE && + new Date().getTime() > getFirstStartTime() + USAGE_REPORTING_DIALOG_DELAY && + getApi().getUsageReportAccepted() == RestApi.UsageReportSetting.UNDECIDED) { + showUsageReportingDialog(); + } else if (currentState != SyncthingService.State.ACTIVE && !isFinishing()) { + if (currentState == SyncthingService.State.DISABLED) { if (mLoadingDialog != null) { mLoadingDialog.dismiss(); mLoadingDialog = null; } - if (mDisabledDialog != null) { - mDisabledDialog.dismiss(); - mDisabledDialog = null; + mDisabledDialog = SyncthingService.showDisabledDialog(MainActivity.this); + } else if (mLoadingDialog == null) { + LayoutInflater inflater = getLayoutInflater(); + View dialogLayout = inflater.inflate(R.layout.loading_dialog, null); + TextView loadingText = (TextView) dialogLayout.findViewById(R.id.loading_text); + loadingText.setText((getService().isFirstStart()) + ? R.string.web_gui_creating_key + : R.string.api_loading); + + mLoadingDialog = new AlertDialog.Builder(MainActivity.this) + .setCancelable(false) + .setView(dialogLayout) + .show(); + + final SharedPreferences sp = + PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + + // Make sure the first start dialog is shown on top. + if (sp.getBoolean("first_start", true)) { + showFirstStartDialog(sp); } - mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - mDrawerLayout.setDrawerListener(mDrawerToggle); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeButtonEnabled(true); } - }); + return; + } + + if (mLoadingDialog != null) { + mLoadingDialog.dismiss(); + mLoadingDialog = null; + } + if (mDisabledDialog != null) { + mDisabledDialog.dismiss(); + mDisabledDialog = null; + } + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); + mDrawerLayout.setDrawerListener(mDrawerToggle); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + } + + /** + * Returns the unix timestamp at which the app was first installed. + */ + @TargetApi(9) + private long getFirstStartTime() { + PackageManager pm = getPackageManager(); + long firstInstallTime = 0; + try { + // No info is available on Froyo. + firstInstallTime = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) + ? pm.getPackageInfo(getPackageName(), 0).firstInstallTime + : 0; + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "This should never happen", e); + } + return firstInstallTime; + } + + /** + * Displays information for first app start. + */ + private void showFirstStartDialog(final SharedPreferences sp) { + new AlertDialog.Builder(MainActivity.this) + .setTitle(R.string.welcome_title) + .setMessage(R.string.welcome_text) + .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + sp.edit().putBoolean("first_start", false).commit(); + } + }) + .show(); } private final FragmentPagerAdapter mSectionsPagerAdapter = @@ -227,7 +268,7 @@ public class MainActivity extends SyncthingActivity } /** - * Saves fragment states. + * Saves current tab index and fragment states. */ @Override protected void onSaveInstanceState(Bundle outState) { @@ -263,7 +304,7 @@ public class MainActivity extends SyncthingActivity /** - * Receives drawer opened and closed events. + * Handles drawer opened and closed events, toggling option menu state. */ public class Toggle extends ActionBarDrawerToggle { public Toggle(Activity activity, DrawerLayout drawerLayout) { @@ -310,4 +351,38 @@ public class MainActivity extends SyncthingActivity return super.onKeyDown(keyCode, e); } + /** + * Displays dialog asking user to accept/deny usage reporting. + */ + @SuppressLint("InflateParams") + private void showUsageReportingDialog() { + final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + getApi().setUsageReportAccepted( + (which == DialogInterface.BUTTON_POSITIVE) + ? RestApi.UsageReportSetting.ACCEPTED + : RestApi.UsageReportSetting.DENIED, + MainActivity.this); + } + }; + + getApi().getUsageReport(new RestApi.OnReceiveUsageReportListener() { + @Override + public void onReceiveUsageReport(String report) { + View v = LayoutInflater.from(MainActivity.this) + .inflate(R.layout.usage_reporting_dialog, null); + TextView tv = (TextView) v.findViewById(R.id.example); + tv.setText(report); + new AlertDialog.Builder(MainActivity.this) + .setTitle(R.string.usage_reporting_dialog_title) + .setView(v) + .setPositiveButton(R.string.yes, listener) + .setNegativeButton(R.string.no, listener) + .setCancelable(false) + .show(); + } + }); + } + } diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java index 5547bfd2..509c5ba9 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java @@ -72,8 +72,8 @@ public class SettingsFragment extends PreferenceFragment value = api.getLocalDevice().name; break; case USAGE_REPORT_ACCEPTED: - String v = api.getValue(RestApi.TYPE_OPTIONS, pref.getKey()); - value = (v.equals("1")) ? "true" : "false"; + RestApi.UsageReportSetting setting = api.getUsageReportAccepted(); + value = Boolean.toString(setting == RestApi.UsageReportSetting.ACCEPTED); break; default: value = api.getValue(RestApi.TYPE_OPTIONS, pref.getKey()); @@ -240,8 +240,10 @@ public class SettingsFragment extends PreferenceFragment updated.name = (String) o; mSyncthingService.getApi().editDevice(updated, getActivity(), null); } else if (preference.getKey().equals(USAGE_REPORT_ACCEPTED)) { - mSyncthingService.getApi().setValue(RestApi.TYPE_OPTIONS, preference.getKey(), - ((Boolean) o) ? 1 : 0, false, getActivity()); + RestApi.UsageReportSetting setting = ((Boolean) o) + ? RestApi.UsageReportSetting.ACCEPTED + : RestApi.UsageReportSetting.DENIED; + mSyncthingService.getApi().setUsageReportAccepted(setting, getActivity()); } else if (mOptionsScreen.findPreference(preference.getKey()) != null) { boolean isArray = preference.getKey().equals("listenAddress") || preference.getKey().equals("globalAnnounceServers"); diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java index cf139ac4..946aff3a 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java @@ -29,17 +29,13 @@ public class GetTask extends AsyncTask { private static final String TAG = "GetTask"; - public static final String URI_CONFIG = "/rest/system/config"; - - public static final String URI_VERSION = "/rest/system/version"; - - public static final String URI_SYSTEM = "/rest/system/status"; - + public static final String URI_CONFIG = "/rest/system/config"; + public static final String URI_VERSION = "/rest/system/version"; + public static final String URI_SYSTEM = "/rest/system/status"; public static final String URI_CONNECTIONS = "/rest/system/connections"; - - public static final String URI_MODEL = "/rest/db/status"; - - public static final String URI_DEVICEID = "/rest/svc/deviceid"; + public static final String URI_MODEL = "/rest/db/status"; + public static final String URI_DEVICEID = "/rest/svc/deviceid"; + public static final String URI_REPORT = "/rest/svc/report"; private String mHttpsCertPath; diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java index 2e9bc1ca..ae2f0580 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java @@ -21,8 +21,7 @@ public class PostTask extends AsyncTask { private static final String TAG = "PostTask"; public static final String URI_CONFIG = "/rest/system/config"; - - public static final String URI_SCAN = "/rest/db/scan"; + public static final String URI_SCAN = "/rest/db/scan"; private String mHttpsCertPath; diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index 75a3acb0..0826c364 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -1017,4 +1017,67 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, return mGuiPassword; } + public enum UsageReportSetting { + UNDECIDED, + ACCEPTED, + DENIED, + } + + /** + * Returns value of usage reporting preference. + */ + public UsageReportSetting getUsageReportAccepted() { + try { + switch (mConfig.getJSONObject(TYPE_OPTIONS).getInt("urAccepted")) { + case 0: return UsageReportSetting.UNDECIDED; + case 1: return UsageReportSetting.ACCEPTED; + case -1: return UsageReportSetting.DENIED; + default: throw new RuntimeException("Invalid usage report value"); + } + } catch (JSONException e) { + Log.w(TAG, "Failed to read usage report value", e); + return UsageReportSetting.DENIED; + } + } + + /** + * Sets new value for usage reporting preference. + */ + public void setUsageReportAccepted(UsageReportSetting value, Activity activity) { + int v = 0; + switch (value) { + case ACCEPTED: v = 1; break; + case DENIED: v = -1; break; + } + try { + mConfig.getJSONObject(TYPE_OPTIONS).put("urAccepted", v); + } catch (JSONException e) { + Log.w(TAG, "Failed to set usage report value", e); + } + requireRestart(activity); + } + + /** + * Callback for {@link #getUsageReport}. + */ + public interface OnReceiveUsageReportListener { + public void onReceiveUsageReport(String report); + } + + /** + * Returns prettyfied usage report. + */ + public void getUsageReport(final OnReceiveUsageReportListener listener) { + new GetTask(mHttpsCertPath) { + @Override + protected void onPostExecute(String s) { + try { + listener.onReceiveUsageReport(new JSONObject(s).toString(4)); + } catch (JSONException e) { + throw new RuntimeException("Failed to prettify usage report", e); + } + } + }.execute(mUrl, GetTask.URI_REPORT, mApiKey); + } + } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java index 1aa86e2c..e0c4078d 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java @@ -315,6 +315,7 @@ public class SyncthingService extends Service implements new RestApi.OnApiAvailableListener() { @Override public void onApiAvailable() { + mCurrentState = State.ACTIVE; onApiChange(); new Thread(new Runnable() { @Override @@ -436,7 +437,7 @@ public class SyncthingService extends Service implements return; } Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl()); - mCurrentState = State.ACTIVE; + mCurrentState = State.STARTING; onApiChange(); for (OnWebGuiAvailableListener listener : mOnWebGuiAvailableListeners) { listener.onWebGuiAvailable(); diff --git a/src/main/res/layout/usage_reporting_dialog.xml b/src/main/res/layout/usage_reporting_dialog.xml new file mode 100644 index 00000000..48b6d21b --- /dev/null +++ b/src/main/res/layout/usage_reporting_dialog.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index 4a6bb87d..7ed73768 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -3,4 +3,5 @@ #ffff4444 #ff33b5e5 #ff99cc00 + #cccccc diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ec8621f7..49ffb70b 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -20,6 +20,14 @@ Loading… + Allow Anonymous Usage Reporting? + + The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.\n\nThe aggregated statistics are publicly available at https://data.syncthing.net. + + Yes + + No +