diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml index ae50418b3..cdffee6a7 100644 --- a/app/src/full/AndroidManifest.xml +++ b/app/src/full/AndroidManifest.xml @@ -168,9 +168,11 @@ android:exported="false" /> diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SDCardScannerService.java b/app/src/full/java/org/fdroid/fdroid/nearby/SDCardScannerService.java index 8fb002189..27cd98328 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SDCardScannerService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SDCardScannerService.java @@ -20,7 +20,6 @@ package org.fdroid.fdroid.nearby; import android.Manifest; -import android.app.IntentService; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -29,6 +28,8 @@ import android.os.Environment; import android.os.Process; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.core.app.JobIntentService; import androidx.core.content.ContextCompat; import org.fdroid.fdroid.Utils; @@ -46,7 +47,7 @@ import java.util.HashSet; import java.util.List; /** - * An {@link IntentService} subclass for scanning removable "external storage" + * An {@link JobIntentService} subclass for scanning removable "external storage" * for F-Droid package repos, e.g. SD Cards. This is intended to support * sharable package repos, so it ignores non-removable storage, like the fake * emulated sdcard from devices with only built-in storage. This method will @@ -63,26 +64,24 @@ import java.util.List; * @see Universal way to write to external SD card on Android * @see The Storage Situation: External Storage */ -public class SDCardScannerService extends IntentService { +public class SDCardScannerService extends JobIntentService { public static final String TAG = "SDCardScannerService"; + private static final int JOB_ID = TAG.hashCode(); private static final String ACTION_SCAN = "org.fdroid.fdroid.nearby.SCAN"; private static final List SKIP_DIRS = Arrays.asList(".android_secure", "LOST.DIR"); - public SDCardScannerService() { - super("SDCardScannerService"); - } public static void scan(Context context) { Intent intent = new Intent(context, SDCardScannerService.class); intent.setAction(ACTION_SCAN); - context.startService(intent); + JobIntentService.enqueueWork(context, SDCardScannerService.class, JOB_ID, intent); } @Override - protected void onHandleIntent(Intent intent) { - if (intent == null || !ACTION_SCAN.equals(intent.getAction())) { + protected void onHandleWork(@NonNull Intent intent) { + if (!ACTION_SCAN.equals(intent.getAction())) { return; } Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); @@ -94,7 +93,14 @@ public class SDCardScannerService extends IntentService { continue; } Log.i(TAG, "getExternalFilesDirs " + f); - if (Environment.isExternalStorageRemovable(f)) { + boolean isExternalStorageRemovable; + try { + isExternalStorageRemovable = Environment.isExternalStorageRemovable(f); + } catch (IllegalArgumentException e) { + Utils.debugLog(TAG, e.toString()); + continue; + } + if (isExternalStorageRemovable) { String state = Environment.getExternalStorageState(f); if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java b/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java index 6966e130b..c8443ebf4 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java @@ -28,6 +28,8 @@ import android.os.Process; import android.util.Log; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.core.app.JobIntentService; import androidx.documentfile.provider.DocumentFile; import org.apache.commons.io.FileUtils; @@ -54,7 +56,7 @@ import java.util.jar.JarFile; import java.util.jar.JarInputStream; /** - * An {@link IntentService} subclass for handling asynchronous scanning of a + * An {@link JobIntentService} subclass for handling asynchronous scanning of a * removable storage device like an SD Card or USB OTG thumb drive using the * Storage Access Framework. Permission must first be granted by the user * {@link android.content.Intent#ACTION_OPEN_DOCUMENT_TREE} or @@ -73,25 +75,23 @@ import java.util.jar.JarInputStream; * @see Using Scoped Directory Access * @see Open Files using Storage Access Framework */ -public class TreeUriScannerIntentService extends IntentService { +public class TreeUriScannerIntentService extends JobIntentService { public static final String TAG = "TreeUriScannerIntentSer"; + private static final int JOB_ID = TAG.hashCode(); private static final String ACTION_SCAN_TREE_URI = "org.fdroid.fdroid.nearby.action.SCAN_TREE_URI"; + /** * @see DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY * @see ExternalStorageProvider.AUTHORITY */ public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"; - public TreeUriScannerIntentService() { - super("TreeUriScannerIntentService"); - } - public static void scan(Context context, Uri data) { Intent intent = new Intent(context, TreeUriScannerIntentService.class); intent.setAction(ACTION_SCAN_TREE_URI); intent.setData(data); - context.startService(intent); + JobIntentService.enqueueWork(context, TreeUriScannerIntentService.class, JOB_ID, intent); } /** @@ -115,8 +115,8 @@ public class TreeUriScannerIntentService extends IntentService { } @Override - protected void onHandleIntent(Intent intent) { - if (intent == null || !ACTION_SCAN_TREE_URI.equals(intent.getAction())) { + protected void onHandleWork(@NonNull Intent intent) { + if (!ACTION_SCAN_TREE_URI.equals(intent.getAction())) { return; } Uri treeUri = intent.getData(); diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index 07ae09d15..f3d44445e 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -49,11 +49,13 @@ import java.util.Set; import java.util.concurrent.TimeUnit; /** - * Handles shared preferences for FDroid, looking after the names of - * preferences, default values and caching. Needs to be setup in the FDroidApp - * (using {@link Preferences#setup(android.content.Context)} before it gets - * accessed via the {@link org.fdroid.fdroid.Preferences#get()} - * singleton method. + * Handles the preferences that are shown the Settings UI, looking after the + * names of preferences, default values and caching. Needs to be setup in the + * {@link org.fdroid.fdroid.FDroidApp} (using + * {@link Preferences#setup(android.content.Context)} before it gets accessed + * via the {@link org.fdroid.fdroid.Preferences#get()} singleton method. This + * structure also lets it be used in places in the code where there is no + * {@link Context}. *

* All defaults should be set in {@code res/xml/preferences.xml}. The one * exception is {@link Preferences#PREF_LOCAL_REPO_NAME} since it needs to be diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 1ae576407..edaa64ec2 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -300,9 +300,13 @@ public final class Utils { "7.1", // 25 "8.0", // 26 "8.1", // 27 - "9.0", // 28 - "10.0", // 29 - "11.0", // 30 + "9", // 28 + "10", // 29 + "11", // 30 + "12", // 31 + "12.1", // 32 + "13", // 33 + "14", // 34 }; public static String getAndroidVersionName(int sdkLevel) { diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java index 719a09dac..e0d66e908 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java @@ -26,6 +26,7 @@ import android.Manifest; import android.app.SearchManager; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; @@ -39,6 +40,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -90,6 +92,8 @@ public class MainActivity extends AppCompatActivity { private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled"; + private static final String BOTTOM_NAVIGATION_MENU_ID = "bottomNavigationMenuId"; + private static final String ACTION_ADD_REPO = "org.fdroid.fdroid.MainActivity.ACTION_ADD_REPO"; public static final String ACTION_REQUEST_SWAP = "requestSwap"; @@ -122,10 +126,14 @@ public class MainActivity extends AppCompatActivity { pager.setAdapter(adapter); bottomNavigation = findViewById(R.id.bottom_navigation); + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + setSelectedMenuInNav(sharedPreferences.getInt(BOTTOM_NAVIGATION_MENU_ID, R.id.latest)); bottomNavigation.setOnNavigationItemSelectedListener(item -> { pager.scrollToPosition(item.getOrder()); - if (item.getItemId() == R.id.nearby) { + final int itemId = item.getItemId(); + sharedPreferences.edit().putInt(BOTTOM_NAVIGATION_MENU_ID, itemId).apply(); + if (itemId == R.id.nearby) { NearbyViewBinder.updateUsbOtg(MainActivity.this); } @@ -140,13 +148,21 @@ public class MainActivity extends AppCompatActivity { AppUpdateStatusManager.getInstance(this).getNumUpdatableApps().observe(this, this::refreshUpdatesBadge); Intent intent = getIntent(); + if (handleMainViewSelectIntent(intent)) { + return; + } handleSearchOrAppViewIntent(intent); } + /** + * {@link android.material.navigation.NavigationBarView} says "Menu items + * can also be used for programmatically selecting which destination is + * currently active. It can be done using {@code MenuItem.setChecked(true)}". + */ private void setSelectedMenuInNav(int menuId) { int position = adapter.adapterPositionFromItemId(menuId); pager.scrollToPosition(position); - bottomNavigation.setSelectedItemId(position); + bottomNavigation.getMenu().getItem(position).setChecked(true); } private void initialRepoUpdateIfRequired() { @@ -162,17 +178,6 @@ public class MainActivity extends AppCompatActivity { FDroidApp.checkStartTor(this, Preferences.get()); - if (getIntent().hasExtra(EXTRA_VIEW_UPDATES)) { - getIntent().removeExtra(EXTRA_VIEW_UPDATES); - setSelectedMenuInNav(R.id.updates); - } else if (getIntent().hasExtra(EXTRA_VIEW_NEARBY)) { - getIntent().removeExtra(EXTRA_VIEW_NEARBY); - setSelectedMenuInNav(R.id.nearby); - } else if (getIntent().hasExtra(EXTRA_VIEW_SETTINGS)) { - getIntent().removeExtra(EXTRA_VIEW_SETTINGS); - setSelectedMenuInNav(R.id.settings); - } - // AppDetailsActivity and RepoDetailsActivity set different NFC actions, so reset here NfcHelper.setAndroidBeam(this, getApplication().getPackageName()); checkForAddRepoIntent(getIntent()); @@ -194,6 +199,11 @@ public class MainActivity extends AppCompatActivity { @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); + + if (handleMainViewSelectIntent(intent)) { + return; + } + handleSearchOrAppViewIntent(intent); // This is called here as well as onResume(), because onNewIntent() is not called the first @@ -231,6 +241,23 @@ public class MainActivity extends AppCompatActivity { } } + /** + * Handle an {@link Intent} that shows a specific tab in the main view. + */ + private boolean handleMainViewSelectIntent(Intent intent) { + if (intent.hasExtra(EXTRA_VIEW_NEARBY)) { + setSelectedMenuInNav(R.id.nearby); + return true; + } else if (intent.hasExtra(EXTRA_VIEW_UPDATES)) { + setSelectedMenuInNav(R.id.updates); + return true; + } else if (intent.hasExtra(EXTRA_VIEW_SETTINGS)) { + setSelectedMenuInNav(R.id.settings); + return true; + } + return false; + } + /** * Since any app could send this {@link Intent}, and the search terms are * fed into a SQL query, the data must be strictly sanitized to avoid