1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-11-29 15:51:17 +00:00

Add welcome slide for secure key generation (#4)

* Add welcome slide "key generation"

* Improve slide icon

* Add key generation via ConfigXml to welcome wizard slide
If key and config files are already present in syncthing's data folder
they won't be overwritten (as ConfigXml checks for that). It's also
no problem to go through the slides again, e.g. if the storage permission
got revoked after the first app launch granting it.

* Remove test mode

* Remove "Enjoy Syncthing."

* Improve string "Consider backing up your sync data"

* Show welcome slides only if mandatory prerequisites are
missing. Show only slides that are necessary because of
missing prerequisites. Mandatory prerequisites are
a) storage permission b) existance of keys and config
Remove key generation UI from StateDialogActivity as this
is no longer required in the main UI as we ensure generating
keys and config before launching to MainActivity.

* Minor review adjustments

* Review - Improve explanation string on config corruption
This commit is contained in:
Catfriend1 2018-08-19 23:10:02 +02:00 committed by GitHub
parent 680eb7dc86
commit b7cfd12c06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 206 additions and 55 deletions

View file

@ -8,6 +8,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.Manifest;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
@ -33,7 +34,9 @@ import android.widget.Toast;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.service.Constants;
import com.nutomic.syncthingandroid.util.ConfigXml;
import java.lang.ref.WeakReference;
import javax.inject.Inject;
public class FirstStartActivity extends Activity {
@ -41,20 +44,39 @@ public class FirstStartActivity extends Activity {
private static String TAG = "FirstStartActivity";
private static final int REQUEST_COARSE_LOCATION = 141;
private static final int REQUEST_WRITE_STORAGE = 142;
private static final int SLIDE_POS_LOCATION_PERMISSION = 1;
private static class Slide {
public int layout;
public int dotColorActive;
public int dotColorInActive;
Slide (int layout, int dotColorActive, int dotColorInActive) {
this.layout = layout;
this.dotColorActive = dotColorActive;
this.dotColorInActive = dotColorInActive;
}
}
private Slide[] mSlides;
/**
* Initialize the slide's ViewPager position to "-1" so it will never
* trigger any action in {@link #onBtnNextClick} if the slide is not
* shown.
*/
private int mSlidePosStoragePermission = -1;
private int mSlidePosKeyGeneration = -1;
private ViewPager mViewPager;
private ViewPagerAdapter mViewPagerAdapter;
private LinearLayout mDotsLayout;
private TextView[] mDots;
private int[] mLayouts;
private Button mBackButton;
private Button mNextButton;
@Inject SharedPreferences mPreferences;
/**
* Handles activity behaviour depending on {@link #isFirstStart()} and {@link #haveStoragePermission()}.
* Handles activity behaviour depending on prerequisites.
*/
@SuppressLint("ClickableViewAccessibility")
@Override
@ -63,11 +85,18 @@ public class FirstStartActivity extends Activity {
((SyncthingApp) getApplication()).component().inject(this);
/**
* Recheck storage permission. If it has been revoked after the user
* completed the welcome slides, displays the slides again.
* Check if prerequisites to run the app are still in place.
* If anything mandatory is missing, the according welcome slide(s) will be shown.
*/
if (!mPreferences.getBoolean(Constants.PREF_FIRST_START, true) &&
haveStoragePermission()) {
Boolean showSlideStoragePermission = !haveStoragePermission();
Boolean showSlideLocationPermission = !haveLocationPermission();
Boolean showSlideKeyGeneration = !Constants.getConfigFile(this).exists();
/**
* If we don't have to show slides for mandatory prerequisites,
* start directly into MainActivity.
*/
if (!showSlideStoragePermission && !showSlideKeyGeneration) {
startApp();
return;
}
@ -94,11 +123,28 @@ public class FirstStartActivity extends Activity {
}
});
// Layouts of all welcome slides
mLayouts = new int[]{
R.layout.activity_firststart_slide1,
R.layout.activity_firststart_slide2,
R.layout.activity_firststart_slide3};
// Add welcome slides to be shown.
int[] colorsActive = getResources().getIntArray(R.array.array_dot_active);
int[] colorsInactive = getResources().getIntArray(R.array.array_dot_inactive);
int slideIndex = 0;
mSlides = new Slide[
1 +
(showSlideStoragePermission ? 1 : 0) +
(showSlideLocationPermission ? 1 : 0) +
(showSlideKeyGeneration ? 1 : 0)
];
mSlides[slideIndex++] = new Slide(R.layout.activity_firststart_intro, colorsActive[0], colorsInactive[0]);
if (showSlideStoragePermission) {
mSlidePosStoragePermission = slideIndex;
mSlides[slideIndex++] = new Slide(R.layout.activity_firststart_storage_permission, colorsActive[1], colorsInactive[1]);
}
if (showSlideLocationPermission) {
mSlides[slideIndex++] = new Slide(R.layout.activity_firststart_location_permission, colorsActive[2], colorsInactive[2]);
}
if (showSlideKeyGeneration) {
mSlidePosKeyGeneration = slideIndex;
mSlides[slideIndex++] = new Slide(R.layout.activity_firststart_key_generation, colorsActive[3], colorsInactive[3]);
}
// Add bottom dots
addBottomDots(0);
@ -138,7 +184,7 @@ public class FirstStartActivity extends Activity {
public void onBtnNextClick() {
// Check if we are allowed to advance to the next slide.
if (mViewPager.getCurrentItem() == SLIDE_POS_LOCATION_PERMISSION) {
if (mViewPager.getCurrentItem() == mSlidePosStoragePermission) {
// As the storage permission is a prerequisite to run syncthing, refuse to continue without it.
if (!haveStoragePermission()) {
Toast.makeText(this, R.string.toast_write_storage_permission_required,
@ -148,10 +194,13 @@ public class FirstStartActivity extends Activity {
}
int current = getItem(+1);
if (current < mLayouts.length) {
if (current < mSlides.length) {
// Move to next slide.
mViewPager.setCurrentItem(current);
mBackButton.setVisibility(View.VISIBLE);
if (current == mSlidePosKeyGeneration) {
onKeyGenerationSlideShown();
}
} else {
// Start the app after "mNextButton" was hit on the last slide.
Log.v(TAG, "User completed first start UI.");
@ -161,22 +210,19 @@ public class FirstStartActivity extends Activity {
}
private void addBottomDots(int currentPage) {
mDots = new TextView[mLayouts.length];
int[] colorsActive = getResources().getIntArray(R.array.array_dot_active);
int[] colorsInactive = getResources().getIntArray(R.array.array_dot_inactive);
mDots = new TextView[mSlides.length];
mDotsLayout.removeAllViews();
for (int i = 0; i < mDots.length; i++) {
mDots[i] = new TextView(this);
mDots[i].setText(Html.fromHtml("&#8226;"));
mDots[i].setTextSize(35);
mDots[i].setTextColor(colorsInactive[currentPage]);
mDots[i].setTextColor(mSlides[currentPage].dotColorInActive);
mDotsLayout.addView(mDots[i]);
}
if (mDots.length > 0)
mDots[currentPage].setTextColor(colorsActive[currentPage]);
mDots[currentPage].setTextColor(mSlides[currentPage].dotColorActive);
}
private int getItem(int i) {
@ -191,7 +237,7 @@ public class FirstStartActivity extends Activity {
addBottomDots(position);
// Change the next button text from next to finish on last slide.
mNextButton.setText(getString((position == mLayouts.length - 1) ? R.string.finish : R.string.cont));
mNextButton.setText(getString((position == mSlides.length - 1) ? R.string.finish : R.string.cont));
}
@Override
@ -229,7 +275,7 @@ public class FirstStartActivity extends Activity {
public Object instantiateItem(ViewGroup container, int position) {
layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = layoutInflater.inflate(mLayouts[position], container, false);
View view = layoutInflater.inflate(mSlides[position].layout, container, false);
/* Slide: storage permission */
Button btnGrantStoragePerm = (Button) view.findViewById(R.id.btnGrantStoragePerm);
@ -259,7 +305,7 @@ public class FirstStartActivity extends Activity {
@Override
public int getCount() {
return mLayouts.length;
return mSlides.length;
}
@Override
@ -279,9 +325,7 @@ public class FirstStartActivity extends Activity {
* Storage permission has been granted.
*/
private void startApp() {
Boolean doInitialKeyGeneration = !Constants.getConfigFile(this).exists();
Intent mainIntent = new Intent(this, MainActivity.class);
mainIntent.putExtra(MainActivity.EXTRA_KEY_GENERATION_IN_PROGRESS, doInitialKeyGeneration);
/**
* In case start_into_web_gui option is enabled, start both activities
@ -298,6 +342,12 @@ public class FirstStartActivity extends Activity {
/**
* Permission check and request functions
*/
private boolean haveLocationPermission() {
int permissionState = ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION);
return permissionState == PackageManager.PERMISSION_GRANTED;
}
private void requestLocationPermission() {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
@ -343,4 +393,55 @@ public class FirstStartActivity extends Activity {
}
}
/**
* Perform secure key generation in an AsyncTask.
*/
private void onKeyGenerationSlideShown() {
mNextButton.setVisibility(View.GONE);
KeyGenerationTask keyGenerationTask = new KeyGenerationTask(this);
keyGenerationTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Sets up the initial configuration and generates secure keys.
*/
private static class KeyGenerationTask extends AsyncTask<Void, Void, Void> {
private WeakReference<FirstStartActivity> refFirstStartActivity;
ConfigXml configXml;
KeyGenerationTask(FirstStartActivity context) {
refFirstStartActivity = new WeakReference<>(context);
}
@Override
protected Void doInBackground(Void... voids) {
FirstStartActivity firstStartActivity = refFirstStartActivity.get();
if (firstStartActivity == null) {
cancel(true);
return null;
}
try {
configXml = new ConfigXml(firstStartActivity);
} catch (ConfigXml.OpenConfigException e) {
TextView keygenStatus = firstStartActivity.findViewById(R.id.key_generation_status);
keygenStatus.setText(firstStartActivity.getString(R.string.config_create_failed));
cancel(true);
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
// Get a reference to the activity if it is still there.
FirstStartActivity firstStartActivity = refFirstStartActivity.get();
if (firstStartActivity == null) {
return;
}
TextView keygenStatus = (TextView) firstStartActivity.findViewById(R.id.key_generation_status);
keygenStatus.setText(firstStartActivity.getString(R.string.key_generation_success));
Button nextButton = (Button) firstStartActivity.findViewById(R.id.btn_next);
nextButton.setVisibility(View.VISIBLE);
}
}
}

View file

@ -109,7 +109,6 @@ public class MainActivity extends StateDialogActivity
case STARTING:
break;
case ACTIVE:
getIntent().putExtra(this.EXTRA_KEY_GENERATION_IN_PROGRESS, false);
showBatteryOptimizationDialogIfNecessary();
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
mDrawerFragment.requestGuiUpdate();

View file

@ -124,17 +124,13 @@ public abstract class StateDialogActivity extends SyncthingActivity {
DialogLoadingBinding binding = DataBindingUtil.inflate(
getLayoutInflater(), R.layout.dialog_loading, null, false);
boolean isGeneratingKeys = getIntent().getBooleanExtra(EXTRA_KEY_GENERATION_IN_PROGRESS, false);
binding.loadingText.setText((isGeneratingKeys)
? R.string.web_gui_creating_key
: R.string.api_loading);
binding.loadingText.setText(R.string.api_loading);
mLoadingDialog = new AlertDialog.Builder(this)
.setCancelable(false)
.setView(binding.getRoot())
.show();
if (!isGeneratingKeys) {
new Handler().postDelayed(() -> {
if (this.isFinishing() || mLoadingDialog == null)
return;
@ -144,7 +140,6 @@ public abstract class StateDialogActivity extends SyncthingActivity {
startActivity(new Intent(this, LogActivity.class)));
}, SLOW_LOADING_TIME);
}
}
private void dismissLoadingDialog() {
Util.dismissDialogSafe(mLoadingDialog, this);

View file

@ -22,8 +22,6 @@ import java.util.LinkedList;
*/
public abstract class SyncthingActivity extends AppCompatActivity implements ServiceConnection {
public static final String EXTRA_KEY_GENERATION_IN_PROGRESS = "com.nutomic.syncthing-android.SyncthingActivity.KEY_GENERATION_IN_PROGRESS";
private SyncthingService mSyncthingService;
private final LinkedList<OnServiceConnectedListener> mServiceConnectedListeners = new LinkedList<>();

View file

@ -337,7 +337,7 @@ public class SyncthingService extends Service {
syncthingService.mConfig = new ConfigXml(syncthingService);
syncthingService.mConfig.updateIfNeeded();
} catch (ConfigXml.OpenConfigException e) {
syncthingService.mNotificationHandler.showCrashedNotification(R.string.config_create_failed, true);
syncthingService.mNotificationHandler.showCrashedNotification(R.string.config_read_failed, true);
synchronized (syncthingService.mStateLock) {
syncthingService.onServiceStateChange(State.ERROR);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_screen4">
<LinearLayout
android:background="@color/bg_screen4"
android:gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/welcome_title"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="30sp"
android:layout_margin="30dp" />
<ImageView
android:layout_width="@dimen/img_width_height"
android:layout_height="@dimen/img_width_height"
android:contentDescription="@null"
android:src="@drawable/ic_secure_key" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/key_generation_title"
android:textColor="@android:color/white"
android:textSize="@dimen/slide_title"
android:textStyle="bold" />
<TextView
android:id="@+id/key_generation_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:paddingLeft="@dimen/desc_padding"
android:paddingRight="@dimen/desc_padding"
android:text="@string/web_gui_creating_key"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="@dimen/slide_desc" />
</LinearLayout>
</RelativeLayout>

View file

@ -44,7 +44,7 @@ Bitte melden Sie auftretende Probleme via GitHub.</string>
<!--Text for FoldersFragment and DevicesFragment loading view-->
<string name="api_loading">Ladevorgang…</string>
<!--Shown instead of web_gui_loading if the key does not exist and has to be created-->
<string name="web_gui_creating_key">Geheimschlüssel werden generiert. Dies kann einige Minuten dauern.</string>
<string name="web_gui_creating_key">Geheimschlüssel werden generiert. Dies kann einige Minuten dauern&#8230;</string>
<string name="syncthing_loading_slow_message">Syncthing lädt ungewöhnlich lange. Bitte die Logs auf Fehler überprüfen.</string>
<!--FoldersFragment-->
<string name="folders_fragment_title">Verzeichnisse</string>

View file

@ -16,32 +16,32 @@
<color name="bg_screen1">#3395ff</color>
<color name="bg_screen2">#20d2bb</color>
<color name="bg_screen3">#c873f4</color>
<!-- <color name="bg_screen4">#f64c73</color> -->
<color name="bg_screen4">#5e7c8b</color>
<!-- dots inactive colors -->
<color name="dot_dark_screen1">#2278d4</color>
<color name="dot_dark_screen2">#14a895</color>
<color name="dot_dark_screen3">#a854d4</color>
<!-- <color name="dot_dark_screen4">#d1395c</color> -->
<color name="dot_dark_screen4">#3e5c6b</color>
<!-- dots active colors -->
<color name="dot_light_screen1">#93c6fd</color>
<color name="dot_light_screen2">#8cf9eb</color>
<color name="dot_light_screen3">#e4b5fc</color>
<!-- <color name="dot_light_screen4">#f98da5</color> -->
<color name="dot_light_screen4">#8eacbb</color>
<array name="array_dot_active">
<item>@color/dot_light_screen1</item>
<item>@color/dot_light_screen2</item>
<item>@color/dot_light_screen3</item>
<!-- <item>@color/dot_light_screen4</item> -->
<item>@color/dot_light_screen4</item>
</array>
<array name="array_dot_inactive">
<item>@color/dot_dark_screen1</item>
<item>@color/dot_dark_screen2</item>
<item>@color/dot_dark_screen3</item>
<!-- <item>@color/dot_dark_screen4</item> -->
<item>@color/dot_dark_screen4</item>
</array>
<!-- FirstStartActivity welcome wizard end -->
</resources>

View file

@ -12,20 +12,24 @@
<string name="welcome_title">Welcome to Syncthing for Android</string>
<!-- Welcome wizard -->
<!-- Slide 1 -->
<!-- Slide "Introduction" -->
<string name="introduction">Introduction</string>
<string name="welcome_text">Syncthing is an open-source file synchronization application.\n\
To share data with other devices, you need to add their unique device IDs to the device list. Afterwards you can select which folders to share with which devices.\n\
Please report any problems you encounter via Github.</string>
<!-- Slide 2 -->
<!-- Slide "Storage Permission" -->
<string name="storage_permission_title">Storage Permission</string>
<string name="storage_permission_desc">Syncthing needs to access your storage to do file synchronization.</string>
<!-- Slide 3 -->
<!-- Slide "Location Permission"" -->
<string name="location_permission_title">Location Permission</string>
<string name="location_permission_desc">Syncthing can be configured to run on selected Wi-Fi networks. Android requires applications to have location permissions to be able look up active Wi-Fi network name, as you can sometimes infer users location from the name of the network they are connected to. If you want to use this feature, press the button above to give the required location permissions to Syncthing. Otherwise you can skip this step.</string>
<!-- Slide "Key Generation" -->
<string name="key_generation_title">Key Generation</string>
<string name="key_generation_success">Secure keys for private data exchange have been successfully generated.</string>
<!-- Generic texts used everywhere -->
<string name="back">Back</string>
<string name="cont">Continue</string>
@ -82,7 +86,7 @@ Please report any problems you encounter via Github.</string>
<string name="api_loading">Loading&#8230;</string>
<!-- Shown instead of web_gui_loading if the key does not exist and has to be created -->
<string name="web_gui_creating_key">Generating secure keys. This may take a few minutes.</string>
<string name="web_gui_creating_key">Generating secure keys. This may take a few minutes&#8230;</string>
<string name="syncthing_loading_slow_message">Syncthing is taking very long to load. Use the logs to check for any errors.</string>
@ -649,8 +653,9 @@ Please report any problems you encounter via Github.</string>
<string name="syncthing_terminated">Syncthing was terminated</string>
<!-- Toast shown if syncthing failed to create a config -->
<string name="config_create_failed">Failed to create config file</string>
<!-- Toast shown if syncthing failed to create or read the config -->
<string name="config_create_failed">Failed to create configuration.</string>
<string name="config_read_failed">Failed to read configuration. Consider backing up data from your sync folders, then clear this app\'s data from Android settings and launch it again.</string>
<!-- Toast shown if a config file crucial to operation is missing -->
<string name="config_file_missing">A config file crucial to operation is missing</string>

@ -1 +1 @@
Subproject commit 7812c2c937ad98c24cb6e0bd56ce1d9b4ab967a4
Subproject commit 6b82538e623feb4df2bef9fc4b37d581de96309a