Show dialog for usage reporting after some time (fixes #273).

This commit is contained in:
Felix Ableitner 2015-05-20 01:07:31 +02:00
parent 9a587505be
commit c69b37bc5c
9 changed files with 240 additions and 72 deletions

View File

@ -1,12 +1,15 @@
package com.nutomic.syncthingandroid.activities; package com.nutomic.syncthingandroid.activities;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager; 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.Tab;
import android.support.v7.app.ActionBar.TabListener; import android.support.v7.app.ActionBar.TabListener;
import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.ActionBarDrawerToggle;
import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -31,15 +35,28 @@ import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.fragments.DevicesFragment; import com.nutomic.syncthingandroid.fragments.DevicesFragment;
import com.nutomic.syncthingandroid.fragments.DrawerFragment; import com.nutomic.syncthingandroid.fragments.DrawerFragment;
import com.nutomic.syncthingandroid.fragments.FoldersFragment; import com.nutomic.syncthingandroid.fragments.FoldersFragment;
import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.syncthing.SyncthingService; 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. * {@link com.nutomic.syncthingandroid.fragments.DrawerFragment} in the navigation drawer.
*/ */
public class MainActivity extends SyncthingActivity public class MainActivity extends SyncthingActivity
implements SyncthingService.OnApiChangeListener { 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 mLoadingDialog;
private AlertDialog mDisabledDialog; private AlertDialog mDisabledDialog;
@ -49,11 +66,12 @@ public class MainActivity extends SyncthingActivity
*/ */
@Override @Override
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
public void onApiChange(final SyncthingService.State currentState) { public void onApiChange(SyncthingService.State currentState) {
runOnUiThread(new Runnable() { if (currentState == SyncthingService.State.ACTIVE &&
@Override new Date().getTime() > getFirstStartTime() + USAGE_REPORTING_DIALOG_DELAY &&
public void run() { getApi().getUsageReportAccepted() == RestApi.UsageReportSetting.UNDECIDED) {
if (currentState != SyncthingService.State.ACTIVE && !isFinishing()) { showUsageReportingDialog();
} else if (currentState != SyncthingService.State.ACTIVE && !isFinishing()) {
if (currentState == SyncthingService.State.DISABLED) { if (currentState == SyncthingService.State.DISABLED) {
if (mLoadingDialog != null) { if (mLoadingDialog != null) {
mLoadingDialog.dismiss(); mLoadingDialog.dismiss();
@ -61,9 +79,6 @@ public class MainActivity extends SyncthingActivity
} }
mDisabledDialog = SyncthingService.showDisabledDialog(MainActivity.this); mDisabledDialog = SyncthingService.showDisabledDialog(MainActivity.this);
} else if (mLoadingDialog == null) { } else if (mLoadingDialog == null) {
final SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
LayoutInflater inflater = getLayoutInflater(); LayoutInflater inflater = getLayoutInflater();
View dialogLayout = inflater.inflate(R.layout.loading_dialog, null); View dialogLayout = inflater.inflate(R.layout.loading_dialog, null);
TextView loadingText = (TextView) dialogLayout.findViewById(R.id.loading_text); TextView loadingText = (TextView) dialogLayout.findViewById(R.id.loading_text);
@ -76,18 +91,12 @@ public class MainActivity extends SyncthingActivity
.setView(dialogLayout) .setView(dialogLayout)
.show(); .show();
final SharedPreferences sp =
PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
// Make sure the first start dialog is shown on top. // Make sure the first start dialog is shown on top.
if (prefs.getBoolean("first_start", true)) { if (sp.getBoolean("first_start", true)) {
new AlertDialog.Builder(MainActivity.this) showFirstStartDialog(sp);
.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; return;
@ -106,7 +115,39 @@ public class MainActivity extends SyncthingActivity
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(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 = private final FragmentPagerAdapter mSectionsPagerAdapter =
@ -227,7 +268,7 @@ public class MainActivity extends SyncthingActivity
} }
/** /**
* Saves fragment states. * Saves current tab index and fragment states.
*/ */
@Override @Override
protected void onSaveInstanceState(Bundle outState) { 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 class Toggle extends ActionBarDrawerToggle {
public Toggle(Activity activity, DrawerLayout drawerLayout) { public Toggle(Activity activity, DrawerLayout drawerLayout) {
@ -310,4 +351,38 @@ public class MainActivity extends SyncthingActivity
return super.onKeyDown(keyCode, e); 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();
}
});
}
} }

View File

@ -72,8 +72,8 @@ public class SettingsFragment extends PreferenceFragment
value = api.getLocalDevice().name; value = api.getLocalDevice().name;
break; break;
case USAGE_REPORT_ACCEPTED: case USAGE_REPORT_ACCEPTED:
String v = api.getValue(RestApi.TYPE_OPTIONS, pref.getKey()); RestApi.UsageReportSetting setting = api.getUsageReportAccepted();
value = (v.equals("1")) ? "true" : "false"; value = Boolean.toString(setting == RestApi.UsageReportSetting.ACCEPTED);
break; break;
default: default:
value = api.getValue(RestApi.TYPE_OPTIONS, pref.getKey()); value = api.getValue(RestApi.TYPE_OPTIONS, pref.getKey());
@ -240,8 +240,10 @@ public class SettingsFragment extends PreferenceFragment
updated.name = (String) o; updated.name = (String) o;
mSyncthingService.getApi().editDevice(updated, getActivity(), null); mSyncthingService.getApi().editDevice(updated, getActivity(), null);
} else if (preference.getKey().equals(USAGE_REPORT_ACCEPTED)) { } else if (preference.getKey().equals(USAGE_REPORT_ACCEPTED)) {
mSyncthingService.getApi().setValue(RestApi.TYPE_OPTIONS, preference.getKey(), RestApi.UsageReportSetting setting = ((Boolean) o)
((Boolean) o) ? 1 : 0, false, getActivity()); ? RestApi.UsageReportSetting.ACCEPTED
: RestApi.UsageReportSetting.DENIED;
mSyncthingService.getApi().setUsageReportAccepted(setting, getActivity());
} else if (mOptionsScreen.findPreference(preference.getKey()) != null) { } else if (mOptionsScreen.findPreference(preference.getKey()) != null) {
boolean isArray = preference.getKey().equals("listenAddress") || boolean isArray = preference.getKey().equals("listenAddress") ||
preference.getKey().equals("globalAnnounceServers"); preference.getKey().equals("globalAnnounceServers");

View File

@ -30,16 +30,12 @@ public class GetTask extends AsyncTask<String, Void, String> {
private static final String TAG = "GetTask"; private static final String TAG = "GetTask";
public static final String URI_CONFIG = "/rest/system/config"; public static final String URI_CONFIG = "/rest/system/config";
public static final String URI_VERSION = "/rest/system/version"; public static final String URI_VERSION = "/rest/system/version";
public static final String URI_SYSTEM = "/rest/system/status"; public static final String URI_SYSTEM = "/rest/system/status";
public static final String URI_CONNECTIONS = "/rest/system/connections"; public static final String URI_CONNECTIONS = "/rest/system/connections";
public static final String URI_MODEL = "/rest/db/status"; public static final String URI_MODEL = "/rest/db/status";
public static final String URI_DEVICEID = "/rest/svc/deviceid"; public static final String URI_DEVICEID = "/rest/svc/deviceid";
public static final String URI_REPORT = "/rest/svc/report";
private String mHttpsCertPath; private String mHttpsCertPath;

View File

@ -21,7 +21,6 @@ public class PostTask extends AsyncTask<String, Void, Boolean> {
private static final String TAG = "PostTask"; private static final String TAG = "PostTask";
public static final String URI_CONFIG = "/rest/system/config"; 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; private String mHttpsCertPath;

View File

@ -1017,4 +1017,67 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
return mGuiPassword; 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);
}
} }

View File

@ -315,6 +315,7 @@ public class SyncthingService extends Service implements
new RestApi.OnApiAvailableListener() { new RestApi.OnApiAvailableListener() {
@Override @Override
public void onApiAvailable() { public void onApiAvailable() {
mCurrentState = State.ACTIVE;
onApiChange(); onApiChange();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
@ -436,7 +437,7 @@ public class SyncthingService extends Service implements
return; return;
} }
Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl()); Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl());
mCurrentState = State.ACTIVE; mCurrentState = State.STARTING;
onApiChange(); onApiChange();
for (OnWebGuiAvailableListener listener : mOnWebGuiAvailableListeners) { for (OnWebGuiAvailableListener listener : mOnWebGuiAvailableListeners) {
listener.onWebGuiAvailable(); listener.onWebGuiAvailable();

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dip">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="10dip"
android:text="@string/usage_reporting_dialog_description" />
<HorizontalScrollView android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/light_grey"
android:padding="5dip"
android:id="@+id/example"/>
</HorizontalScrollView>
</LinearLayout>

View File

@ -3,4 +3,5 @@
<color name="text_red">#ffff4444</color> <color name="text_red">#ffff4444</color>
<color name="text_blue">#ff33b5e5</color> <color name="text_blue">#ff33b5e5</color>
<color name="text_green">#ff99cc00</color> <color name="text_green">#ff99cc00</color>
<color name="light_grey">#cccccc</color>
</resources> </resources>

View File

@ -20,6 +20,14 @@
<!-- Text for FoldersFragment and DevicesFragment loading view --> <!-- Text for FoldersFragment and DevicesFragment loading view -->
<string name="api_loading">Loading&#8230;</string> <string name="api_loading">Loading&#8230;</string>
<string name="usage_reporting_dialog_title">Allow Anonymous Usage Reporting?</string>
<string name="usage_reporting_dialog_description">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.</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<!-- FoldersFragment --> <!-- FoldersFragment -->