1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-12-01 16:51:16 +00:00

Bump android api level to 30 (ref #1721) (#1724)

This commit is contained in:
Simon Frei 2021-12-23 18:32:28 +01:00 committed by GitHub
parent abbe8afd2d
commit 81698e1acc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 348 additions and 138 deletions

View file

@ -12,6 +12,7 @@ dependencies {
implementation 'com.google.guava:guava:31.0.1-android' implementation 'com.google.guava:guava:31.0.1-android'
implementation 'com.annimon:stream:1.2.2' implementation 'com.annimon:stream:1.2.2'
implementation 'com.android.volley:volley:1.2.1' 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.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
implementation 'com.google.zxing:core:3.4.1' implementation 'com.google.zxing:core:3.4.1'
@ -25,12 +26,12 @@ dependencies {
android { android {
// Changes to these values need to be reflected in `../docker/Dockerfile` // Changes to these values need to be reflected in `../docker/Dockerfile`
compileSdkVersion 29 compileSdkVersion 30
buildToolsVersion '29.0.3' buildToolsVersion '30.0.0'
ndkVersion = "${ndkVersionShared}" ndkVersion = "${ndkVersionShared}"
buildTypes.debug.applicationIdSuffix ".debug" buildTypes.debug.applicationIdSuffix ".debug"
dataBinding.enabled = true buildFeatures.dataBinding = true
playConfigs { playConfigs {
defaultAccountConfig { defaultAccountConfig {
@ -40,7 +41,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.nutomic.syncthingandroid" applicationId "com.nutomic.syncthingandroid"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 30
versionCode 4290 versionCode 4290
versionName "1.18.4-rc.2" versionName "1.18.4-rc.2"
testApplicationId 'com.nutomic.syncthingandroid.test' testApplicationId 'com.nutomic.syncthingandroid.test'

View file

@ -25,6 +25,8 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- CAMERA is required for the QR Code Scanner --> <!-- CAMERA is required for the QR Code Scanner -->
<uses-permission android:name="android.permission.CAMERA" /> <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" /> <uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
@ -43,7 +45,7 @@
<activity <activity
android:name=".activities.FirstStartActivity" android:name=".activities.FirstStartActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleInstance"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View file

@ -1,13 +1,17 @@
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.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Color; import android.graphics.Color;
import android.Manifest; import android.Manifest;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -15,7 +19,9 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import androidx.appcompat.app.AppCompatActivity;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.text.Html; import android.text.Html;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -33,28 +39,43 @@ import android.widget.Toast;
import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.SyncthingApp; import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.service.Constants; 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; import javax.inject.Inject;
public class FirstStartActivity extends Activity { 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 ViewPager mViewPager;
private ViewPagerAdapter mViewPagerAdapter; private ViewPagerAdapter mViewPagerAdapter;
private LinearLayout mDotsLayout; private LinearLayout mDotsLayout;
private TextView[] mDots; private TextView[] mDots;
private int[] mLayouts;
private Button mBackButton; private Button mBackButton;
private Button mNextButton; private Button mNextButton;
@Inject SharedPreferences mPreferences; @Inject
SharedPreferences mPreferences;
/**
* Handles activity behaviour depending on {@link #isFirstStart()} and {@link #haveStoragePermission()}.
*/
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@Override @Override
protected void onCreate(Bundle savedInstanceState) { 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 * Recheck storage permission. If it has been revoked after the user
* completed the welcome slides, displays the slides again. * completed the welcome slides, displays the slides again.
*/ */
if (!mPreferences.getBoolean(Constants.PREF_FIRST_START, true) && if (!isFirstStart() && PermissionUtil.haveStoragePermission(this) && upgradedToApiLevel30()) {
haveStoragePermission()) {
startApp(); startApp();
return; 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 // Add bottom dots
addBottomDots(0); addBottomDots();
setActiveBottomDot(0);
// Make notification bar transparent // Make notification bar transparent
changeStatusBarColor(); changeStatusBarColor();
@ -122,10 +137,15 @@ public class FirstStartActivity extends Activity {
onBtnNextClick(); onBtnNextClick();
} }
}); });
if (!isFirstStart()) {
// Skip intro slide
onBtnNextClick();
}
} }
public void onBtnBackClick() { public void onBtnBackClick() {
int current = getItem(-1); int current = mViewPager.getCurrentItem() - 1;
if (current >= 0) { if (current >= 0) {
// Move to previous slider. // Move to previous slider.
mViewPager.setCurrentItem(current); mViewPager.setCurrentItem(current);
@ -136,22 +156,37 @@ public class FirstStartActivity extends Activity {
} }
public void onBtnNextClick() { public void onBtnNextClick() {
Slide slide = currentSlide();
// Check if we are allowed to advance to the next slide. // 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. // 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.makeText(this, R.string.toast_write_storage_permission_required,
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
return; 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); int next = mViewPager.getCurrentItem() + 1;
if (current < mLayouts.length) { while (next < slides.length) {
// Move to next slide. if (!shouldSkipSlide(slides[next])) {
mViewPager.setCurrentItem(current); mViewPager.setCurrentItem(next);
mBackButton.setVisibility(View.VISIBLE); mBackButton.setVisibility(View.VISIBLE);
} else { break;
}
next++;
}
if (next == slides.length) {
// Start the app after "mNextButton" was hit on the last slide. // Start the app after "mNextButton" was hit on the last slide.
Log.v(TAG, "User completed first start UI."); Log.v(TAG, "User completed first start UI.");
mPreferences.edit().putBoolean(Constants.PREF_FIRST_START, false).apply(); mPreferences.edit().putBoolean(Constants.PREF_FIRST_START, false).apply();
@ -159,27 +194,63 @@ public class FirstStartActivity extends Activity {
} }
} }
private void addBottomDots(int currentPage) { private boolean isFirstStart() {
mDots = new TextView[mLayouts.length]; return mPreferences.getBoolean(Constants.PREF_FIRST_START, true);
}
int[] colorsActive = getResources().getIntArray(R.array.array_dot_active); private boolean upgradedToApiLevel30() {
int[] colorsInactive = getResources().getIntArray(R.array.array_dot_inactive); 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++) { for (int i = 0; i < mDots.length; i++) {
mDots[i] = new TextView(this); mDots[i] = new TextView(this);
mDots[i].setText(Html.fromHtml("&#8226;")); mDots[i].setText(Html.fromHtml("&#8226;"));
mDots[i].setTextSize(35); mDots[i].setTextSize(35);
mDots[i].setTextColor(colorsInactive[currentPage]);
mDotsLayout.addView(mDots[i]); mDotsLayout.addView(mDots[i]);
} }
if (mDots.length > 0)
mDots[currentPage].setTextColor(colorsActive[currentPage]);
} }
private int getItem(int i) { private void setActiveBottomDot(int currentPage) {
return mViewPager.getCurrentItem() + i; 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 // ViewPager change listener
@ -187,10 +258,10 @@ public class FirstStartActivity extends Activity {
@Override @Override
public void onPageSelected(int position) { public void onPageSelected(int position) {
addBottomDots(position); setActiveBottomDot(position);
// Change the next button text from next to finish on last slide. // 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 @Override
@ -228,28 +299,42 @@ public class FirstStartActivity extends Activity {
public Object instantiateItem(ViewGroup container, int position) { public Object instantiateItem(ViewGroup container, int position) {
layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 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); Button btnGrantStoragePerm = (Button) view.findViewById(R.id.btnGrantStoragePerm);
if (btnGrantStoragePerm != null) {
btnGrantStoragePerm.setOnClickListener(new View.OnClickListener() { btnGrantStoragePerm.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
requestStoragePermission(); requestStoragePermission();
} }
}); });
} break;
/* Slide: location permission */ case LOCATION:
Button btnGrantLocationPerm = (Button) view.findViewById(R.id.btnGrantLocationPerm); Button btnGrantLocationPerm = (Button) view.findViewById(R.id.btnGrantLocationPerm);
if (btnGrantLocationPerm != null) {
btnGrantLocationPerm.setOnClickListener(new View.OnClickListener() { btnGrantLocationPerm.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
requestLocationPermission(); 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); container.addView(view);
@ -258,7 +343,7 @@ public class FirstStartActivity extends Activity {
@Override @Override
public int getCount() { public int getCount() {
return mLayouts.length; return slides.length;
} }
@Override @Override
@ -281,7 +366,6 @@ public class FirstStartActivity extends Activity {
Boolean doInitialKeyGeneration = !Constants.getConfigFile(this).exists(); Boolean doInitialKeyGeneration = !Constants.getConfigFile(this).exists();
Intent mainIntent = new Intent(this, MainActivity.class); Intent mainIntent = new Intent(this, MainActivity.class);
mainIntent.putExtra(MainActivity.EXTRA_KEY_GENERATION_IN_PROGRESS, doInitialKeyGeneration); mainIntent.putExtra(MainActivity.EXTRA_KEY_GENERATION_IN_PROGRESS, doInitialKeyGeneration);
/** /**
* In case start_into_web_gui option is enabled, start both activities * In case start_into_web_gui option is enabled, start both activities
* so that back navigation works as expected. * so that back navigation works as expected.
@ -294,48 +378,75 @@ public class FirstStartActivity extends Activity {
finish(); 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 * Permission check and request functions
*/ */
private void requestLocationPermission() { private void requestLocationPermission() {
ActivityCompat.requestPermissions(this, ActivityCompat.requestPermissions(this,
Constants.getLocationPermissions(), PermissionUtil.getLocationPermissions(),
Constants.PermissionRequestType.LOCATION.ordinal()); 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() { private void requestStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
requestAllFilesAccessPermission();
} else {
ActivityCompat.requestPermissions(this, ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
Constants.PermissionRequestType.STORAGE.ordinal()); 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 @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) { @NonNull int[] grantResults) {
switch (Constants.PermissionRequestType.values()[requestCode]) { switch (Constants.PermissionRequestType.values()[requestCode]) {
case LOCATION: case LOCATION:
boolean granted = grantResults.length != 0; if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
if (!granted) { Log.i(TAG, "User denied foreground location permission");
Log.i(TAG, "No location permission in request-result");
break; break;
} }
for (int i = 0; i < grantResults.length; i++) { Log.i(TAG, "User granted foreground location permission");
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.i(TAG, "User granted permission: " + permissions[i]); ActivityCompat.requestPermissions(this,
} else { PermissionUtil.getLocationPermissions(),
granted = false; Constants.PermissionRequestType.LOCATION_BACKGROUND.ordinal());
Log.i(TAG, "User denied permission: " + permissions[i]);
} }
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(); Toast.makeText(this, R.string.permission_granted, Toast.LENGTH_SHORT).show();
}
break; break;
case STORAGE: case STORAGE:
if (grantResults.length == 0 || if (grantResults.length == 0 ||
@ -350,5 +461,4 @@ public class FirstStartActivity extends Activity {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults);
} }
} }
} }

View file

@ -14,7 +14,6 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.Manifest;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -25,7 +24,6 @@ import com.google.android.material.tabs.TabLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentPagerAdapter;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import androidx.drawerlayout.widget.DrawerLayout; 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.DeviceListFragment;
import com.nutomic.syncthingandroid.fragments.DrawerFragment; import com.nutomic.syncthingandroid.fragments.DrawerFragment;
import com.nutomic.syncthingandroid.fragments.FolderListFragment; 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.RestApi;
import com.nutomic.syncthingandroid.service.SyncthingService; import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.service.SyncthingServiceBinder; import com.nutomic.syncthingandroid.service.SyncthingServiceBinder;
import com.nutomic.syncthingandroid.util.PermissionUtil;
import com.nutomic.syncthingandroid.util.Util; import com.nutomic.syncthingandroid.util.Util;
import java.util.Date; import java.util.Date;
@ -270,8 +269,7 @@ public class MainActivity extends StateDialogActivity
@Override @Override
public void onResume() { public void onResume() {
// Check if storage permission has been revoked at runtime. // Check if storage permission has been revoked at runtime.
if ((ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != if (!PermissionUtil.haveStoragePermission(this)) {
PackageManager.PERMISSION_GRANTED)) {
startActivity(new Intent(this, FirstStartActivity.class)); startActivity(new Intent(this, FirstStartActivity.class));
this.finish(); this.finish();
} }

View file

@ -34,6 +34,7 @@ public class Constants {
public static final String PREF_USE_TOR = "use_tor"; 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_SOCKS_PROXY_ADDRESS = "socks_proxy_address";
public static final String PREF_HTTP_PROXY_ADDRESS = "http_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} * 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. * These are the request codes used when requesting the permissions.
*/ */
public enum PermissionRequestType { public enum PermissionRequestType {
LOCATION, STORAGE LOCATION, LOCATION_BACKGROUND, 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,
};
} }

View file

@ -18,6 +18,7 @@ import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask; import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask;
import com.nutomic.syncthingandroid.model.RunConditionCheckResult; import com.nutomic.syncthingandroid.model.RunConditionCheckResult;
import com.nutomic.syncthingandroid.util.ConfigXml; import com.nutomic.syncthingandroid.util.ConfigXml;
import com.nutomic.syncthingandroid.util.PermissionUtil;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -203,9 +204,7 @@ public class SyncthingService extends Service {
* see issue: https://github.com/syncthing/syncthing-android/issues/871 * see issue: https://github.com/syncthing/syncthing-android/issues/871
* We need to recheck if we still have the storage permission. * We need to recheck if we still have the storage permission.
*/ */
mStoragePermissionGranted = (ContextCompat.checkSelfPermission(this, mStoragePermissionGranted = PermissionUtil.haveStoragePermission(this);
Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED);
if (mNotificationHandler != null) { if (mNotificationHandler != null) {
mNotificationHandler.setAppShutdownInProgress(false); 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. // 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."); Log.w(TAG, "launchStartupTask: StartupTask is still running. Skipped starting it twice.");
return; return;
} }
@ -350,6 +349,10 @@ public class SyncthingService extends Service {
mStartupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 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 * Sets up the initial configuration, and updates the config when coming from an old
* version. * version.
@ -546,6 +549,14 @@ public class SyncthingService extends Service {
} }
mSyncthingRunnable = null; 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(); onKilledListener.onKilled();
} }

View file

@ -107,7 +107,6 @@ public class FileUtils {
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
Method getUuid = storageVolumeClazz.getMethod("getUuid"); Method getUuid = storageVolumeClazz.getMethod("getUuid");
Method getPath = storageVolumeClazz.getMethod("getPath");
Method isPrimary = storageVolumeClazz.getMethod("isPrimary"); Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
Object result = getVolumeList.invoke(mStorageManager); Object result = getVolumeList.invoke(mStorageManager);
@ -127,7 +126,7 @@ public class FileUtils {
if (isPrimaryVolume || isExternalVolume) { if (isPrimaryVolume || isExternalVolume) {
Log.v(TAG, "getVolumePath: isPrimaryVolume || isExternalVolume"); Log.v(TAG, "getVolumePath: isPrimaryVolume || isExternalVolume");
// Return path if the correct volume corresponding to volumeId was found. // Return path if the correct volume corresponding to volumeId was found.
return (String) getPath.invoke(storageVolumeElement); return volumeToPath(storageVolumeElement, storageVolumeClazz);
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -137,6 +136,21 @@ public class FileUtils {
return null; 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 * FileProvider does not support converting the absolute path from
* getExternalFilesDir() to a "content://" Uri. As "file://" Uri * getExternalFilesDir() to a "content://" Uri. As "file://" Uri

View file

@ -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;
}
}

View file

@ -75,7 +75,6 @@ public class Util {
} }
/** /**
* <<<<<<< HEAD
* Normally an application's data directory is only accessible by the corresponding application. * 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, * 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. * it writes to the application's data directory. This leaves files and directories behind which are owned by root having 0600.

View file

@ -1,13 +1,10 @@
package com.nutomic.syncthingandroid.views; package com.nutomic.syncthingandroid.views;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo; import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.MultiSelectListPreference; import android.preference.MultiSelectListPreference;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
@ -17,8 +14,8 @@ import android.widget.Toast;
import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.service.Constants; import com.nutomic.syncthingandroid.service.Constants;
import com.nutomic.syncthingandroid.util.PermissionUtil;
import java.util.Arrays;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -103,7 +100,7 @@ public class WifiSsidPreference extends MultiSelectListPreference {
if (!hasPerms && context instanceof Activity) { if (!hasPerms && context instanceof Activity) {
Activity activity = (Activity) context; 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. * Checks if the required location permissions to obtain WiFi SSID are granted.
*/ */
private boolean hasLocationPermissions() { private boolean hasLocationPermissions() {
String[] perms = Constants.getLocationPermissions(); String[] perms = PermissionUtil.getLocationPermissions();
for (int i = 0; i < perms.length; i++) { for (int i = 0; i < perms.length; i++) {
if (ContextCompat.checkSelfPermission(getContext(), perms[i]) != PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(getContext(), perms[i]) != PackageManager.PERMISSION_GRANTED) {
return false; return false;

View file

@ -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>

View file

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

View file

@ -21,11 +21,19 @@ Please report any problems you encounter via Github.</string>
<!-- Slide 2 --> <!-- Slide 2 -->
<string name="storage_permission_title">Storage Permission</string> <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="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 --> <!-- Slide 3 -->
<string name="location_permission_title">Location Permission (background)</string> <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> <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 --> <!-- Generic texts used everywhere -->
<string name="back">Back</string> <string name="back">Back</string>
<string name="cont">Continue</string> <string name="cont">Continue</string>