mirror of
https://github.com/syncthing/syncthing-android.git
synced 2024-11-25 22:01:16 +00:00
parent
abbe8afd2d
commit
81698e1acc
16 changed files with 348 additions and 138 deletions
|
@ -12,6 +12,7 @@ dependencies {
|
|||
implementation 'com.google.guava:guava:31.0.1-android'
|
||||
implementation 'com.annimon:stream:1.2.2'
|
||||
implementation 'com.android.volley:volley:1.2.1'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
|
||||
implementation ('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
|
@ -25,12 +26,12 @@ dependencies {
|
|||
|
||||
android {
|
||||
// Changes to these values need to be reflected in `../docker/Dockerfile`
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '30.0.0'
|
||||
ndkVersion = "${ndkVersionShared}"
|
||||
|
||||
buildTypes.debug.applicationIdSuffix ".debug"
|
||||
dataBinding.enabled = true
|
||||
buildFeatures.dataBinding = true
|
||||
|
||||
playConfigs {
|
||||
defaultAccountConfig {
|
||||
|
@ -40,7 +41,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.nutomic.syncthingandroid"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode 4290
|
||||
versionName "1.18.4-rc.2"
|
||||
testApplicationId 'com.nutomic.syncthingandroid.test'
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<!-- CAMERA is required for the QR Code Scanner -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- MANAGE_EXTERNAL_STORAGE is required on Android 11 "R" -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||
|
||||
|
@ -43,7 +45,7 @@
|
|||
<activity
|
||||
android:name=".activities.FirstStartActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleInstance">
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
package com.nutomic.syncthingandroid.activities;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.Manifest;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -15,7 +19,9 @@ import androidx.core.app.ActivityCompat;
|
|||
import androidx.core.content.ContextCompat;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.Html;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -33,28 +39,43 @@ 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.PermissionUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class FirstStartActivity extends Activity {
|
||||
|
||||
private static String TAG = "FirstStartActivity";
|
||||
private enum Slide {
|
||||
INTRO(R.layout.activity_firststart_slide_intro),
|
||||
STORAGE(R.layout.activity_firststart_slide_storage),
|
||||
LOCATION(R.layout.activity_firststart_slide_location),
|
||||
API_LEVEL_30(R.layout.activity_firststart_slide_api_level_30);
|
||||
|
||||
private static final int SLIDE_POS_LOCATION_PERMISSION = 1;
|
||||
public final int layout;
|
||||
|
||||
Slide(int layout) {
|
||||
this.layout = layout;
|
||||
}
|
||||
};
|
||||
|
||||
private static Slide[] slides = Slide.values();
|
||||
private static String TAG = "FirstStartActivity";
|
||||
|
||||
private ViewPager mViewPager;
|
||||
private ViewPagerAdapter mViewPagerAdapter;
|
||||
private LinearLayout mDotsLayout;
|
||||
private TextView[] mDots;
|
||||
private int[] mLayouts;
|
||||
private Button mBackButton;
|
||||
private Button mNextButton;
|
||||
|
||||
@Inject SharedPreferences mPreferences;
|
||||
@Inject
|
||||
SharedPreferences mPreferences;
|
||||
|
||||
/**
|
||||
* Handles activity behaviour depending on {@link #isFirstStart()} and {@link #haveStoragePermission()}.
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -65,8 +86,7 @@ public class FirstStartActivity extends Activity {
|
|||
* Recheck storage permission. If it has been revoked after the user
|
||||
* completed the welcome slides, displays the slides again.
|
||||
*/
|
||||
if (!mPreferences.getBoolean(Constants.PREF_FIRST_START, true) &&
|
||||
haveStoragePermission()) {
|
||||
if (!isFirstStart() && PermissionUtil.haveStoragePermission(this) && upgradedToApiLevel30()) {
|
||||
startApp();
|
||||
return;
|
||||
}
|
||||
|
@ -93,14 +113,9 @@ 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 bottom dots
|
||||
addBottomDots(0);
|
||||
addBottomDots();
|
||||
setActiveBottomDot(0);
|
||||
|
||||
// Make notification bar transparent
|
||||
changeStatusBarColor();
|
||||
|
@ -122,10 +137,15 @@ public class FirstStartActivity extends Activity {
|
|||
onBtnNextClick();
|
||||
}
|
||||
});
|
||||
|
||||
if (!isFirstStart()) {
|
||||
// Skip intro slide
|
||||
onBtnNextClick();
|
||||
}
|
||||
}
|
||||
|
||||
public void onBtnBackClick() {
|
||||
int current = getItem(-1);
|
||||
int current = mViewPager.getCurrentItem() - 1;
|
||||
if (current >= 0) {
|
||||
// Move to previous slider.
|
||||
mViewPager.setCurrentItem(current);
|
||||
|
@ -136,22 +156,37 @@ public class FirstStartActivity extends Activity {
|
|||
}
|
||||
|
||||
public void onBtnNextClick() {
|
||||
Slide slide = currentSlide();
|
||||
// Check if we are allowed to advance to the next slide.
|
||||
if (mViewPager.getCurrentItem() == SLIDE_POS_LOCATION_PERMISSION) {
|
||||
switch (slide) {
|
||||
case STORAGE:
|
||||
// As the storage permission is a prerequisite to run syncthing, refuse to continue without it.
|
||||
if (!haveStoragePermission()) {
|
||||
Boolean storagePermissionsGranted = PermissionUtil.haveStoragePermission(this);
|
||||
if (!storagePermissionsGranted) {
|
||||
Toast.makeText(this, R.string.toast_write_storage_permission_required,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case API_LEVEL_30:
|
||||
if (!upgradedToApiLevel30()) {
|
||||
Toast.makeText(this, R.string.toast_api_level_30_must_reset,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
int current = getItem(+1);
|
||||
if (current < mLayouts.length) {
|
||||
// Move to next slide.
|
||||
mViewPager.setCurrentItem(current);
|
||||
int next = mViewPager.getCurrentItem() + 1;
|
||||
while (next < slides.length) {
|
||||
if (!shouldSkipSlide(slides[next])) {
|
||||
mViewPager.setCurrentItem(next);
|
||||
mBackButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
next++;
|
||||
}
|
||||
if (next == slides.length) {
|
||||
// Start the app after "mNextButton" was hit on the last slide.
|
||||
Log.v(TAG, "User completed first start UI.");
|
||||
mPreferences.edit().putBoolean(Constants.PREF_FIRST_START, false).apply();
|
||||
|
@ -159,27 +194,63 @@ public class FirstStartActivity extends Activity {
|
|||
}
|
||||
}
|
||||
|
||||
private void addBottomDots(int currentPage) {
|
||||
mDots = new TextView[mLayouts.length];
|
||||
private boolean isFirstStart() {
|
||||
return mPreferences.getBoolean(Constants.PREF_FIRST_START, true);
|
||||
}
|
||||
|
||||
int[] colorsActive = getResources().getIntArray(R.array.array_dot_active);
|
||||
int[] colorsInactive = getResources().getIntArray(R.array.array_dot_inactive);
|
||||
private boolean upgradedToApiLevel30() {
|
||||
if (mPreferences.getBoolean(Constants.PREF_UPGRADED_TO_API_LEVEL_30, false)) {
|
||||
return true;
|
||||
}
|
||||
if (isFirstStart()) {
|
||||
mPreferences.edit().putBoolean(Constants.PREF_UPGRADED_TO_API_LEVEL_30, true).apply();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
mDotsLayout.removeAllViews();
|
||||
private void upgradeToApiLevel30() {
|
||||
FileUtils.deleteQuietly(new File(this.getFilesDir(), "index-v0.14.0.db"));
|
||||
mPreferences.edit().putBoolean(Constants.PREF_UPGRADED_TO_API_LEVEL_30, true).apply();
|
||||
}
|
||||
|
||||
private Slide currentSlide() {
|
||||
return slides[mViewPager.getCurrentItem()];
|
||||
}
|
||||
|
||||
private boolean shouldSkipSlide(Slide slide) {
|
||||
switch (slide) {
|
||||
case INTRO:
|
||||
return !isFirstStart();
|
||||
case STORAGE:
|
||||
return PermissionUtil.haveStoragePermission(this);
|
||||
case LOCATION:
|
||||
return hasLocationPermission();
|
||||
case API_LEVEL_30:
|
||||
// Skip if running as root, as that circumvents any Android FS restrictions.
|
||||
return upgradedToApiLevel30()
|
||||
|| PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_USE_ROOT, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void addBottomDots() {
|
||||
mDots = new TextView[slides.length];
|
||||
for (int i = 0; i < mDots.length; i++) {
|
||||
mDots[i] = new TextView(this);
|
||||
mDots[i].setText(Html.fromHtml("•"));
|
||||
mDots[i].setTextSize(35);
|
||||
mDots[i].setTextColor(colorsInactive[currentPage]);
|
||||
mDotsLayout.addView(mDots[i]);
|
||||
}
|
||||
|
||||
if (mDots.length > 0)
|
||||
mDots[currentPage].setTextColor(colorsActive[currentPage]);
|
||||
}
|
||||
|
||||
private int getItem(int i) {
|
||||
return mViewPager.getCurrentItem() + i;
|
||||
private void setActiveBottomDot(int currentPage) {
|
||||
int[] colorsActive = getResources().getIntArray(R.array.array_dot_active);
|
||||
int[] colorsInactive = getResources().getIntArray(R.array.array_dot_inactive);
|
||||
for (int i = 0; i < mDots.length; i++) {
|
||||
mDots[i].setTextColor(colorsInactive[currentPage]);
|
||||
}
|
||||
mDots[currentPage].setTextColor(colorsActive[currentPage]);
|
||||
}
|
||||
|
||||
// ViewPager change listener
|
||||
|
@ -187,10 +258,10 @@ public class FirstStartActivity extends Activity {
|
|||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
addBottomDots(position);
|
||||
setActiveBottomDot(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 == slides.length - 1) ? R.string.finish : R.string.cont));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -228,28 +299,42 @@ 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(slides[position].layout, container, false);
|
||||
|
||||
/* Slide: storage permission */
|
||||
switch (slides[position]) {
|
||||
case INTRO:
|
||||
break;
|
||||
|
||||
case STORAGE:
|
||||
Button btnGrantStoragePerm = (Button) view.findViewById(R.id.btnGrantStoragePerm);
|
||||
if (btnGrantStoragePerm != null) {
|
||||
btnGrantStoragePerm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
requestStoragePermission();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
/* Slide: location permission */
|
||||
case LOCATION:
|
||||
Button btnGrantLocationPerm = (Button) view.findViewById(R.id.btnGrantLocationPerm);
|
||||
if (btnGrantLocationPerm != null) {
|
||||
btnGrantLocationPerm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
requestLocationPermission();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case API_LEVEL_30:
|
||||
Button btnResetDatabase = (Button) view.findViewById(R.id.btnResetDatabase);
|
||||
btnResetDatabase.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
upgradeToApiLevel30();
|
||||
onBtnNextClick();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
container.addView(view);
|
||||
|
@ -258,7 +343,7 @@ public class FirstStartActivity extends Activity {
|
|||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mLayouts.length;
|
||||
return slides.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -281,7 +366,6 @@ public class FirstStartActivity extends Activity {
|
|||
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
|
||||
* so that back navigation works as expected.
|
||||
|
@ -294,48 +378,75 @@ public class FirstStartActivity extends Activity {
|
|||
finish();
|
||||
}
|
||||
|
||||
private boolean hasLocationPermission() {
|
||||
for (String perm : PermissionUtil.getLocationPermissions()) {
|
||||
if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission check and request functions
|
||||
*/
|
||||
private void requestLocationPermission() {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
Constants.getLocationPermissions(),
|
||||
PermissionUtil.getLocationPermissions(),
|
||||
Constants.PermissionRequestType.LOCATION.ordinal());
|
||||
}
|
||||
|
||||
private boolean haveStoragePermission() {
|
||||
int permissionState = ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
return permissionState == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
requestAllFilesAccessPermission();
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
Constants.PermissionRequestType.STORAGE.ordinal());
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(30)
|
||||
private void requestAllFilesAccessPermission() {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||
try {
|
||||
ComponentName componentName = intent.resolveActivity(getPackageManager());
|
||||
if (componentName != null) {
|
||||
// Launch "Allow all files access?" dialog.
|
||||
startActivity(intent);
|
||||
return;
|
||||
}
|
||||
Log.w(TAG, "Request all files access not supported");
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Request all files access not supported", e);
|
||||
}
|
||||
Toast.makeText(this, R.string.dialog_all_files_access_not_supported, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
switch (Constants.PermissionRequestType.values()[requestCode]) {
|
||||
case LOCATION:
|
||||
boolean granted = grantResults.length != 0;
|
||||
if (!granted) {
|
||||
Log.i(TAG, "No location permission in request-result");
|
||||
if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(TAG, "User denied foreground location permission");
|
||||
break;
|
||||
}
|
||||
for (int i = 0; i < grantResults.length; i++) {
|
||||
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(TAG, "User granted permission: " + permissions[i]);
|
||||
} else {
|
||||
granted = false;
|
||||
Log.i(TAG, "User denied permission: " + permissions[i]);
|
||||
Log.i(TAG, "User granted foreground location permission");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
PermissionUtil.getLocationPermissions(),
|
||||
Constants.PermissionRequestType.LOCATION_BACKGROUND.ordinal());
|
||||
}
|
||||
break;
|
||||
case LOCATION_BACKGROUND:
|
||||
if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(TAG, "User denied background location permission");
|
||||
break;
|
||||
}
|
||||
if (granted) {
|
||||
Log.i(TAG, "User granted background location permission");
|
||||
Toast.makeText(this, R.string.permission_granted, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
break;
|
||||
case STORAGE:
|
||||
if (grantResults.length == 0 ||
|
||||
|
@ -350,5 +461,4 @@ public class FirstStartActivity extends Activity {
|
|||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import android.content.pm.PackageManager;
|
|||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.Manifest;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
@ -25,7 +24,6 @@ import com.google.android.material.tabs.TabLayout;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
@ -49,10 +47,11 @@ import com.nutomic.syncthingandroid.SyncthingApp;
|
|||
import com.nutomic.syncthingandroid.fragments.DeviceListFragment;
|
||||
import com.nutomic.syncthingandroid.fragments.DrawerFragment;
|
||||
import com.nutomic.syncthingandroid.fragments.FolderListFragment;
|
||||
import com.nutomic.syncthingandroid.model.Options;
|
||||
import com.nutomic.syncthingandroid.service.Constants;
|
||||
import com.nutomic.syncthingandroid.service.RestApi;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingServiceBinder;
|
||||
import com.nutomic.syncthingandroid.util.PermissionUtil;
|
||||
import com.nutomic.syncthingandroid.util.Util;
|
||||
|
||||
import java.util.Date;
|
||||
|
@ -270,8 +269,7 @@ public class MainActivity extends StateDialogActivity
|
|||
@Override
|
||||
public void onResume() {
|
||||
// Check if storage permission has been revoked at runtime.
|
||||
if ((ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
|
||||
PackageManager.PERMISSION_GRANTED)) {
|
||||
if (!PermissionUtil.haveStoragePermission(this)) {
|
||||
startActivity(new Intent(this, FirstStartActivity.class));
|
||||
this.finish();
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ public class Constants {
|
|||
public static final String PREF_USE_TOR = "use_tor";
|
||||
public static final String PREF_SOCKS_PROXY_ADDRESS = "socks_proxy_address";
|
||||
public static final String PREF_HTTP_PROXY_ADDRESS = "http_proxy_address";
|
||||
public static final String PREF_UPGRADED_TO_API_LEVEL_30 = "upgraded_to_api_level_30";
|
||||
|
||||
/**
|
||||
* Available options cache for preference {@link app_settings#debug_facilities_enabled}
|
||||
|
@ -52,28 +53,7 @@ public class Constants {
|
|||
* These are the request codes used when requesting the permissions.
|
||||
*/
|
||||
public enum PermissionRequestType {
|
||||
LOCATION, STORAGE
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location permissions required to access wifi SSIDs depending
|
||||
* on the respective Android version.
|
||||
*/
|
||||
public static String[] getLocationPermissions() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { // before android 9
|
||||
return new String[]{
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
};
|
||||
}
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { // android 9
|
||||
return new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
};
|
||||
}
|
||||
return new String[]{ // after android 9
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
|
||||
};
|
||||
LOCATION, LOCATION_BACKGROUND, STORAGE
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.nutomic.syncthingandroid.SyncthingApp;
|
|||
import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask;
|
||||
import com.nutomic.syncthingandroid.model.RunConditionCheckResult;
|
||||
import com.nutomic.syncthingandroid.util.ConfigXml;
|
||||
import com.nutomic.syncthingandroid.util.PermissionUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
@ -203,9 +204,7 @@ public class SyncthingService extends Service {
|
|||
* see issue: https://github.com/syncthing/syncthing-android/issues/871
|
||||
* We need to recheck if we still have the storage permission.
|
||||
*/
|
||||
mStoragePermissionGranted = (ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
|
||||
PackageManager.PERMISSION_GRANTED);
|
||||
mStoragePermissionGranted = PermissionUtil.haveStoragePermission(this);
|
||||
|
||||
if (mNotificationHandler != null) {
|
||||
mNotificationHandler.setAppShutdownInProgress(false);
|
||||
|
@ -341,7 +340,7 @@ public class SyncthingService extends Service {
|
|||
}
|
||||
|
||||
// Safety check: Log warning if a previously launched startup task did not finish properly.
|
||||
if (mStartupTask != null && (mStartupTask.getStatus() == AsyncTask.Status.RUNNING)) {
|
||||
if (startupTaskIsRunning()) {
|
||||
Log.w(TAG, "launchStartupTask: StartupTask is still running. Skipped starting it twice.");
|
||||
return;
|
||||
}
|
||||
|
@ -350,6 +349,10 @@ public class SyncthingService extends Service {
|
|||
mStartupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private boolean startupTaskIsRunning() {
|
||||
return mStartupTask != null && mStartupTask.getStatus() == AsyncTask.Status.RUNNING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the initial configuration, and updates the config when coming from an old
|
||||
* version.
|
||||
|
@ -546,6 +549,14 @@ public class SyncthingService extends Service {
|
|||
}
|
||||
mSyncthingRunnable = null;
|
||||
}
|
||||
if (startupTaskIsRunning()) {
|
||||
mStartupTask.cancel(true);
|
||||
Log.v(TAG, "Waiting for mStartupTask to finish after cancelling ...");
|
||||
try {
|
||||
mStartupTask.get();
|
||||
} catch (Exception e) { }
|
||||
mStartupTask = null;
|
||||
}
|
||||
onKilledListener.onKilled();
|
||||
}
|
||||
|
||||
|
|
|
@ -107,7 +107,6 @@ public class FileUtils {
|
|||
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
|
||||
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
|
||||
Method getUuid = storageVolumeClazz.getMethod("getUuid");
|
||||
Method getPath = storageVolumeClazz.getMethod("getPath");
|
||||
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
|
||||
Object result = getVolumeList.invoke(mStorageManager);
|
||||
|
||||
|
@ -127,7 +126,7 @@ public class FileUtils {
|
|||
if (isPrimaryVolume || isExternalVolume) {
|
||||
Log.v(TAG, "getVolumePath: isPrimaryVolume || isExternalVolume");
|
||||
// Return path if the correct volume corresponding to volumeId was found.
|
||||
return (String) getPath.invoke(storageVolumeElement);
|
||||
return volumeToPath(storageVolumeElement, storageVolumeClazz);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@ -137,6 +136,21 @@ public class FileUtils {
|
|||
return null;
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@TargetApi(21)
|
||||
private static String volumeToPath(Object storageVolumeElement, Class<?> storageVolumeClazz) throws Exception {
|
||||
try {
|
||||
// >= API level 30
|
||||
Method getDir = storageVolumeClazz.getMethod("getDirectory");
|
||||
File file = (File) getDir.invoke(storageVolumeElement);
|
||||
return file.getPath();
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Not present in API level 30, available at some earlier point.
|
||||
Method getPath = storageVolumeClazz.getMethod("getPath");
|
||||
return (String) getPath.invoke(storageVolumeElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FileProvider does not support converting the absolute path from
|
||||
* getExternalFilesDir() to a "content://" Uri. As "file://" Uri
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package com.nutomic.syncthingandroid.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class PermissionUtil {
|
||||
private PermissionUtil() {}
|
||||
|
||||
/**
|
||||
* Returns the location permissions required to access wifi SSIDs depending
|
||||
* on the respective Android version.
|
||||
*/
|
||||
public static String[] getLocationPermissions() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { // before android 9
|
||||
return new String[]{
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
};
|
||||
}
|
||||
return new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean haveStoragePermission(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return Environment.isExternalStorageManager();
|
||||
}
|
||||
int permissionState = ContextCompat.checkSelfPermission(context,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
return permissionState == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
}
|
|
@ -75,7 +75,6 @@ public class Util {
|
|||
}
|
||||
|
||||
/**
|
||||
* <<<<<<< HEAD
|
||||
* Normally an application's data directory is only accessible by the corresponding application.
|
||||
* Therefore, every file and directory is owned by an application's user and group. When running Syncthing as root,
|
||||
* it writes to the application's data directory. This leaves files and directories behind which are owned by root having 0600.
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
package com.nutomic.syncthingandroid.views;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.wifi.WifiConfiguration;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.MultiSelectListPreference;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
@ -17,8 +14,8 @@ import android.widget.Toast;
|
|||
|
||||
import com.nutomic.syncthingandroid.R;
|
||||
import com.nutomic.syncthingandroid.service.Constants;
|
||||
import com.nutomic.syncthingandroid.util.PermissionUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -103,7 +100,7 @@ public class WifiSsidPreference extends MultiSelectListPreference {
|
|||
|
||||
if (!hasPerms && context instanceof Activity) {
|
||||
Activity activity = (Activity) context;
|
||||
ActivityCompat.requestPermissions(activity, Constants.getLocationPermissions(), Constants.PermissionRequestType.LOCATION.ordinal());
|
||||
ActivityCompat.requestPermissions(activity, PermissionUtil.getLocationPermissions(), Constants.PermissionRequestType.LOCATION.ordinal());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,7 +108,7 @@ public class WifiSsidPreference extends MultiSelectListPreference {
|
|||
* Checks if the required location permissions to obtain WiFi SSID are granted.
|
||||
*/
|
||||
private boolean hasLocationPermissions() {
|
||||
String[] perms = Constants.getLocationPermissions();
|
||||
String[] perms = PermissionUtil.getLocationPermissions();
|
||||
for (int i = 0; i < perms.length; i++) {
|
||||
if (ContextCompat.checkSelfPermission(getContext(), perms[i]) != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bg_screen4"
|
||||
android:fillViewport="true"
|
||||
android:paddingBottom="@dimen/dots_full_height">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="30dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/api_level_30_title"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="30sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="@dimen/desc_marginTop"
|
||||
android:paddingLeft="@dimen/desc_padding"
|
||||
android:paddingRight="@dimen/desc_padding"
|
||||
android:paddingBottom="@dimen/desc_padding"
|
||||
android:text="@string/api_level_30_desc"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="@dimen/slide_desc" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnResetDatabase"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:paddingStart="40dp"
|
||||
android:paddingLeft="40dp"
|
||||
android:paddingEnd="40dp"
|
||||
android:paddingRight="40dp"
|
||||
android:text="@string/api_level_30_button"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -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">#f64c73</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">#d1395c</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">#f98da5</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>
|
||||
|
|
|
@ -21,11 +21,19 @@ Please report any problems you encounter via Github.</string>
|
|||
<!-- Slide 2 -->
|
||||
<string name="storage_permission_title">Storage Permission</string>
|
||||
<string name="storage_permission_desc">Syncthing needs to access your storage to do file synchronization.</string>
|
||||
<string name="dialog_all_files_access_not_supported">Your device does not support all files access</string>
|
||||
|
||||
<!-- Slide 3 -->
|
||||
<string name="location_permission_title">Location Permission (background)</string>
|
||||
<string name="location_permission_desc">Syncthing can be configured to synchronize on selected Wi-Fi networks only. To do that it needs to look up the SSID of the currently connected Wi-Fi, also when the app is closed. Therefore it requires permission to access the location in the background. This information is not used to look up your location. And the only stored information is the Wi-Fi you manually choose to add to the list - no other user data is stored.\n\n If you want to use this feature, press the button above to grant the required permissions to Syncthing. Otherwise you can skip this step.</string>
|
||||
|
||||
<!-- Slide 4 -->
|
||||
<string name="api_level_30_title">Upgrade Information</string>
|
||||
<string name="api_level_30_desc">With the latest update syncthing-android is targeting API level 30, which changes the behaviour of Android\'s filesystem. Good news: On Android 11 and newer it is now able to write to an external SD card. Bad news: It may be slower and some paths will be hidden. Due to these hidden paths, the database needs to be reset to prevent data loss. This will cause all your data to be hashed again, but it doesn\'t need to be transferred from remote devices. It also may create conflicts and re-create recently deleted files if you aren\'t fully in sync right now. Depending on how much data you have, this process can be very resource intensive, i.e. take some time and use a lot of battery power.\n\n Please press the button below to reset the database and start Syncthing.</string>
|
||||
|
||||
<string name="api_level_30_button">Reset database</string>
|
||||
<string name="toast_api_level_30_must_reset">You can only continue by clicking the button to reset the database.</string>
|
||||
|
||||
<!-- Generic texts used everywhere -->
|
||||
<string name="back">Back</string>
|
||||
<string name="cont">Continue</string>
|
||||
|
|
Loading…
Reference in a new issue