diff --git a/app/build.gradle b/app/build.gradle index 36822d85..ead8640e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d925360e..f0704c47 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,8 @@ + + @@ -43,7 +45,7 @@ + android:launchMode="singleTask"> diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java index 5b7aafdf..4c48172e 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java @@ -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) { - // 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, - Toast.LENGTH_LONG).show(); - return; - } + switch (slide) { + case STORAGE: + // As the storage permission is a prerequisite to run syncthing, refuse to continue without it. + 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); - mBackButton.setVisibility(View.VISIBLE); - } else { + int next = mViewPager.getCurrentItem() + 1; + while (next < slides.length) { + if (!shouldSkipSlide(slides[next])) { + mViewPager.setCurrentItem(next); + mBackButton.setVisibility(View.VISIBLE); + 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 */ - Button btnGrantStoragePerm = (Button) view.findViewById(R.id.btnGrantStoragePerm); - if (btnGrantStoragePerm != null) { - btnGrantStoragePerm.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - requestStoragePermission(); - } - }); - } + switch (slides[position]) { + case INTRO: + break; - /* Slide: location permission */ - Button btnGrantLocationPerm = (Button) view.findViewById(R.id.btnGrantLocationPerm); - if (btnGrantLocationPerm != null) { - btnGrantLocationPerm.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - requestLocationPermission(); - } - }); + case STORAGE: + Button btnGrantStoragePerm = (Button) view.findViewById(R.id.btnGrantStoragePerm); + btnGrantStoragePerm.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + requestStoragePermission(); + } + }); + break; + + case LOCATION: + Button btnGrantLocationPerm = (Button) view.findViewById(R.id.btnGrantLocationPerm); + 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,25 +378,50 @@ 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()); + } } - private void requestStoragePermission() { - 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 @@ -320,22 +429,24 @@ public class FirstStartActivity extends Activity { @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()); } - if (granted) { - Toast.makeText(this, R.string.permission_granted, Toast.LENGTH_SHORT).show(); + break; + case LOCATION_BACKGROUND: + if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { + Log.i(TAG, "User denied background location permission"); + break; } + 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); } } - } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java index 1f9aaa6f..e88d74c7 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java @@ -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(); } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java b/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java index 66cef47e..8284740f 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java @@ -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 } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java index 279b7404..6387f683 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java @@ -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(); } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java b/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java index 03effda2..03ac4dd2 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java @@ -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 diff --git a/app/src/main/java/com/nutomic/syncthingandroid/util/PermissionUtil.java b/app/src/main/java/com/nutomic/syncthingandroid/util/PermissionUtil.java new file mode 100644 index 00000000..a793dd50 --- /dev/null +++ b/app/src/main/java/com/nutomic/syncthingandroid/util/PermissionUtil.java @@ -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; + } +} diff --git a/app/src/main/java/com/nutomic/syncthingandroid/util/Util.java b/app/src/main/java/com/nutomic/syncthingandroid/util/Util.java index 5c5f954f..db50cee8 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/util/Util.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/util/Util.java @@ -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. diff --git a/app/src/main/java/com/nutomic/syncthingandroid/views/WifiSsidPreference.java b/app/src/main/java/com/nutomic/syncthingandroid/views/WifiSsidPreference.java index 1cade676..c592b0fc 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/views/WifiSsidPreference.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/views/WifiSsidPreference.java @@ -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; diff --git a/app/src/main/res/layout/activity_firststart_slide_api_level_30.xml b/app/src/main/res/layout/activity_firststart_slide_api_level_30.xml new file mode 100644 index 00000000..76b7f189 --- /dev/null +++ b/app/src/main/res/layout/activity_firststart_slide_api_level_30.xml @@ -0,0 +1,52 @@ + + + + + + + + + +