Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2022-11-17 02:32:35 +01:00
commit 5c10198f68
16 changed files with 191 additions and 74 deletions

View file

@ -24,8 +24,8 @@ package com.nextcloud.client.network
import android.accounts.AccountManager
import android.content.Context
import android.net.ConnectivityManager
import android.os.Build
import com.nextcloud.client.account.UserAccountManagerImpl
import com.nextcloud.client.core.ClockImpl
import com.nextcloud.client.network.ConnectivityServiceImpl.GetRequestBuilder
import com.owncloud.android.AbstractOnServerIT
import org.junit.Assert.assertFalse
@ -40,13 +40,14 @@ class ConnectivityServiceImplIT : AbstractOnServerIT() {
val userAccountManager = UserAccountManagerImpl(targetContext, accountManager)
val clientFactory = ClientFactoryImpl(targetContext)
val requestBuilder = GetRequestBuilder()
val walledCheckCache = WalledCheckCache(ClockImpl())
val sut = ConnectivityServiceImpl(
connectivityManager,
userAccountManager,
clientFactory,
requestBuilder,
Build.VERSION.SDK_INT
walledCheckCache
)
assertTrue(sut.connectivity.isConnected)

View file

@ -77,6 +77,7 @@ class FileMenuFilterIT : AbstractIT() {
every { mockOperationsServiceBinder.isSynchronizing(any(), any()) } returns false
every { mockComponentsGetter.operationsServiceBinder } returns mockOperationsServiceBinder
every { mockStorageManager.getFileById(any()) } returns OCFile("/")
every { mockStorageManager.getFolderContent(any(), any()) } returns ArrayList<OCFile>()
}
@Test

View file

@ -39,11 +39,14 @@ import kotlin.jvm.functions.Function1;
class ConnectivityServiceImpl implements ConnectivityService {
private static final String TAG = "ConnectivityServiceImpl";
private static final String CONNECTIVITY_CHECK_ROUTE = "/index.php/204";
private final ConnectivityManager platformConnectivityManager;
private final UserAccountManager accountManager;
private final ClientFactory clientFactory;
private final GetRequestBuilder requestBuilder;
private final int sdkVersion;
private final WalledCheckCache walledCheckCache;
static class GetRequestBuilder implements Function1<String, GetMethod> {
@Override
@ -56,37 +59,49 @@ class ConnectivityServiceImpl implements ConnectivityService {
UserAccountManager accountManager,
ClientFactory clientFactory,
GetRequestBuilder requestBuilder,
int sdkVersion) {
final WalledCheckCache walledCheckCache) {
this.platformConnectivityManager = platformConnectivityManager;
this.accountManager = accountManager;
this.clientFactory = clientFactory;
this.requestBuilder = requestBuilder;
this.sdkVersion = sdkVersion;
this.walledCheckCache = walledCheckCache;
}
@Override
public boolean isInternetWalled() {
Connectivity c = getConnectivity();
if (c.isConnected() && c.isWifi() && !c.isMetered()) {
final Boolean cachedValue = walledCheckCache.getValue();
if (cachedValue != null) {
return cachedValue;
} else {
boolean result;
Connectivity c = getConnectivity();
if (c.isConnected() && c.isWifi() && !c.isMetered()) {
Server server = accountManager.getUser().getServer();
String baseServerAddress = server.getUri().toString();
if (baseServerAddress.isEmpty()) {
return true;
Server server = accountManager.getUser().getServer();
String baseServerAddress = server.getUri().toString();
if (baseServerAddress.isEmpty()) {
result = true;
} else {
GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
PlainClient client = clientFactory.createPlainClient();
int status = get.execute(client);
// Content-Length is not available when using chunked transfer encoding, so check for -1 as well
result = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
get.releaseConnection();
if (result) {
Log_OC.w(TAG, "isInternetWalled(): Failed to GET " + CONNECTIVITY_CHECK_ROUTE + "," +
" assuming connectivity is impaired");
}
}
} else {
result = !c.isConnected();
}
GetMethod get = requestBuilder.invoke(baseServerAddress + "/index.php/204");
PlainClient client = clientFactory.createPlainClient();
int status = get.execute(client);
// Content-Length is not available when using chunked transfer encoding, so check for -1 as well
boolean result = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
get.releaseConnection();
walledCheckCache.setValue(result);
return result;
} else {
return !c.isConnected();
}
}

View file

@ -37,12 +37,13 @@ public class NetworkModule {
@Provides
ConnectivityService connectivityService(ConnectivityManager connectivityManager,
UserAccountManager accountManager,
ClientFactory clientFactory) {
ClientFactory clientFactory,
WalledCheckCache walledCheckCache) {
return new ConnectivityServiceImpl(connectivityManager,
accountManager,
clientFactory,
new ConnectivityServiceImpl.GetRequestBuilder(),
Build.VERSION.SDK_INT
walledCheckCache
);
}

View file

@ -0,0 +1,67 @@
/*
* Nextcloud Android client application
*
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.nextcloud.client.network
import com.nextcloud.client.core.Clock
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WalledCheckCache @Inject constructor(private val clock: Clock) {
private var cachedEntry: Pair<Long, Boolean>? = null
@Synchronized
fun isExpired(): Boolean {
return when (val timestamp = cachedEntry?.first) {
null -> true
else -> {
val diff = clock.currentTime - timestamp
diff >= CACHE_TIME_MS
}
}
}
@Synchronized
fun setValue(isWalled: Boolean) {
this.cachedEntry = Pair(clock.currentTime, isWalled)
}
@Synchronized
fun getValue(): Boolean? {
return when (isExpired()) {
true -> null
else -> cachedEntry?.second
}
}
@Synchronized
fun clear() {
cachedEntry = null
}
companion object {
// 10 minutes
private const val CACHE_TIME_MS = 10 * 60 * 1000
}
}

View file

@ -54,6 +54,7 @@ import com.nextcloud.client.logger.LegacyLoggerAdapter;
import com.nextcloud.client.logger.Logger;
import com.nextcloud.client.migrations.MigrationsManager;
import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.network.WalledCheckCache;
import com.nextcloud.client.onboarding.OnboardingService;
import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.client.preferences.AppPreferencesImpl;
@ -178,6 +179,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
@Inject
PassCodeManager passCodeManager;
@Inject WalledCheckCache walledCheckCache;
// workaround because injection is initialized on onAttachBaseContext
// and getApplicationContext is null at that point, which crashes when getting current user
@Inject Provider<ViewThemeUtils> viewThemeUtilsProvider;
@ -326,7 +329,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
powerManagementService,
backgroundJobManager,
clock,
viewThemeUtils);
viewThemeUtils,
walledCheckCache);
initContactsBackup(accountManager, backgroundJobManager);
notificationChannels();
@ -506,8 +510,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
final PowerManagementService powerManagementService,
final BackgroundJobManager backgroundJobManager,
final Clock clock,
final ViewThemeUtils viewThemeUtils
) {
final ViewThemeUtils viewThemeUtils,
final WalledCheckCache walledCheckCache) {
updateToAutoUpload();
cleanOldEntries(clock);
updateAutoUploadEntries(clock);
@ -537,7 +541,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
ReceiversHelper.registerNetworkChangeReceiver(uploadsStorageManager,
accountManager,
connectivityService,
powerManagementService);
powerManagementService,
walledCheckCache);
ReceiversHelper.registerPowerChangeReceiver(uploadsStorageManager,
accountManager,

View file

@ -32,6 +32,7 @@ import com.nextcloud.client.core.Clock;
import com.nextcloud.client.device.PowerManagementService;
import com.nextcloud.client.jobs.BackgroundJobManager;
import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.network.WalledCheckCache;
import com.nextcloud.client.preferences.AppPreferences;
import com.owncloud.android.MainApp;
import com.owncloud.android.datamodel.UploadsStorageManager;
@ -59,6 +60,7 @@ public class BootupBroadcastReceiver extends BroadcastReceiver {
@Inject BackgroundJobManager backgroundJobManager;
@Inject Clock clock;
@Inject ViewThemeUtils viewThemeUtils;
@Inject WalledCheckCache walledCheckCache;
/**
* Receives broadcast intent reporting that the system was just boot up. *
@ -78,7 +80,8 @@ public class BootupBroadcastReceiver extends BroadcastReceiver {
powerManagementService,
backgroundJobManager,
clock,
viewThemeUtils);
viewThemeUtils,
walledCheckCache);
MainApp.initContactsBackup(accountManager, backgroundJobManager);
} else {
Log_OC.d(TAG, "Getting wrong intent: " + intent.getAction());

View file

@ -594,7 +594,11 @@ public class FileMenuFilter {
if (isSingleSelection()) {
OCFile file = files.iterator().next();
return file.isFolder() && file.getFileLength() == EMPTY_FILE_LENGTH;
boolean noChildren = componentsGetter
.getStorageManager()
.getFolderContent(file, false).size() == EMPTY_FILE_LENGTH;
return file.isFolder() && file.getFileLength() == EMPTY_FILE_LENGTH && noChildren;
} else {
return false;
}

View file

@ -1102,7 +1102,8 @@ public class FileUploader extends Service
return true;
}
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || context.getResources().getBoolean(R.bool.is_beta);
// bump min version down with every release until minSDK is reached, at that point get rid of old upload code
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || context.getResources().getBoolean(R.bool.is_beta);
}
@VisibleForTesting

View file

@ -1149,7 +1149,7 @@ public class FileDisplayActivity extends FileActivity
localBroadcastManager.registerReceiver(mDownloadFinishReceiver, downloadIntentFilter);
// setup drawer
menuItemId = getIntent().getIntExtra(FileDisplayActivity.DRAWER_MENU_ID, menuItemId);
menuItemId = getIntent().getIntExtra(FileDisplayActivity.DRAWER_MENU_ID, -1);
if (menuItemId == -1) {
if (MainApp.isOnlyOnDevice()) {

View file

@ -33,10 +33,8 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import com.google.android.material.button.MaterialButton;
import com.nextcloud.client.account.User;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.preferences.AppPreferences;
@ -263,13 +261,6 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
mToolbarSpinner.setVisibility(View.VISIBLE);
}
@Override
protected void onResume() {
super.onResume();
requestPermissions();
}
private void fillDirectoryDropdown() {
File currentDir = mCurrentDir;
while (currentDir != null && currentDir.getParentFile() != null) {
@ -677,12 +668,9 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
@Override
protected void onStart() {
super.onStart();
if (getAccount() != null) {
if (!mAccountOnCreation.equals(getAccount())) {
setResult(RESULT_CANCELED);
finish();
}
final Account account = getAccount();
if (mAccountOnCreation != null && mAccountOnCreation.equals(account)) {
requestPermissions();
} else {
setResult(RESULT_CANCELED);
finish();

View file

@ -1536,6 +1536,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
public void onMessageEvent(ChangeMenuEvent changeMenuEvent) {
searchFragment = false;
searchEvent = null;
currentSearchType = SearchType.NO_SEARCH;
menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH;
Activity activity = getActivity();
@ -1739,7 +1740,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
final Context context = getContext();
if (actionBar != null && context != null) {
viewThemeUtils.files.themeActionBar(context, actionBar, title);
viewThemeUtils.files.themeActionBar(context, actionBar, title, true);
}
}
});

View file

@ -131,6 +131,7 @@ object PermissionUtil {
requestStoragePermission(
activity,
Manifest.permission.READ_EXTERNAL_STORAGE,
permissionRequired,
viewThemeUtils
)
}
@ -138,6 +139,7 @@ object PermissionUtil {
else -> requestStoragePermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
permissionRequired,
viewThemeUtils
)
}
@ -151,33 +153,40 @@ object PermissionUtil {
private fun requestStoragePermission(
activity: Activity,
permission: String,
permissionRequired: Boolean,
viewThemeUtils: ViewThemeUtils
) {
val preferences: AppPreferences = AppPreferencesImpl.fromContext(activity)
val shouldRequestPermission = !preferences.isStoragePermissionRequested || permissionRequired
fun doRequest() {
ActivityCompat.requestPermissions(
activity,
arrayOf(permission),
PERMISSIONS_EXTERNAL_STORAGE
)
preferences.isStoragePermissionRequested = true
}
// Check if we should show an explanation
if (shouldShowRequestPermissionRationale(activity, permission)) {
// Show explanation to the user and then request permission
Snackbar
.make(
activity.findViewById(android.R.id.content),
R.string.permission_storage_access,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.common_ok) {
doRequest()
}
.also { viewThemeUtils.material.themeSnackbar(it) }
.show()
} else {
// No explanation needed, request the permission.
doRequest()
if (shouldRequestPermission) {
// Check if we should show an explanation
if (shouldShowRequestPermissionRationale(activity, permission)) {
// Show explanation to the user and then request permission
Snackbar
.make(
activity.findViewById(android.R.id.content),
R.string.permission_storage_access,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.common_ok) {
doRequest()
}
.also { viewThemeUtils.material.themeSnackbar(it) }
.show()
} else {
// No explanation needed, request the permission.
doRequest()
}
}
}
@ -228,6 +237,7 @@ object PermissionUtil {
requestStoragePermission(
activity,
Manifest.permission.READ_EXTERNAL_STORAGE,
permissionRequired,
viewThemeUtils
)
}

View file

@ -30,6 +30,7 @@ import android.content.IntentFilter;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.device.PowerManagementService;
import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.network.WalledCheckCache;
import com.nextcloud.common.DNSCache;
import com.owncloud.android.MainApp;
import com.owncloud.android.datamodel.UploadsStorageManager;
@ -46,7 +47,8 @@ public final class ReceiversHelper {
public static void registerNetworkChangeReceiver(final UploadsStorageManager uploadsStorageManager,
final UserAccountManager accountManager,
final ConnectivityService connectivityService,
final PowerManagementService powerManagementService) {
final PowerManagementService powerManagementService,
final WalledCheckCache walledCheckCache) {
Context context = MainApp.getAppContext();
IntentFilter intentFilter = new IntentFilter();
@ -57,6 +59,7 @@ public final class ReceiversHelper {
@Override
public void onReceive(Context context, Intent intent) {
DNSCache.clear();
walledCheckCache.clear();
if (connectivityService.getConnectivity().isConnected()) {
FilesSyncHelper.restartJobsIfNeeded(uploadsStorageManager,
accountManager,

View file

@ -135,14 +135,25 @@ class FilesSpecificViewThemeUtils @Inject constructor(
return androidViewThemeUtils.tintPrimaryDrawable(context, thumbDrawable)!!
}
private fun getHomeAsUpIcon(isMenu: Boolean): Int {
val icon = if (isMenu) {
R.drawable.ic_menu
} else {
R.drawable.ic_arrow_back
}
return icon
}
/**
* Sets title and colors the actionbar, the title and the back arrow
*/
// TODO move back arrow resource to lib and use lib method directly?
fun themeActionBar(context: Context, actionBar: ActionBar, title: String) {
@JvmOverloads
fun themeActionBar(context: Context, actionBar: ActionBar, title: String, isMenu: Boolean = false) {
val icon = getHomeAsUpIcon(isMenu)
val backArrow = ResourcesCompat.getDrawable(
context.resources,
R.drawable.ic_arrow_back,
icon,
null
)!!
androidXViewThemeUtils.themeActionBar(
@ -156,22 +167,25 @@ class FilesSpecificViewThemeUtils @Inject constructor(
/**
* Sets title and colors the actionbar, the title and the back arrow
*/
fun themeActionBar(context: Context, actionBar: ActionBar, @StringRes titleRes: Int) {
@JvmOverloads
fun themeActionBar(context: Context, actionBar: ActionBar, @StringRes titleRes: Int, isMenu: Boolean = false) {
val title = context.getString(titleRes)
themeActionBar(
context,
actionBar,
title
title,
isMenu
)
}
/**
* Colors actionbar background and back arrow but not the title
*/
fun themeActionBar(context: Context, actionBar: ActionBar) {
@JvmOverloads
fun themeActionBar(context: Context, actionBar: ActionBar, isMenu: Boolean = false) {
val backArrow = ResourcesCompat.getDrawable(
context.resources,
R.drawable.ic_arrow_back,
getHomeAsUpIcon(isMenu),
null
)!!
androidXViewThemeUtils.themeActionBar(context, actionBar, backArrow)

View file

@ -23,7 +23,6 @@ import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkInfo
import android.os.Build
import com.nextcloud.client.account.Server
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
@ -97,6 +96,9 @@ class ConnectivityServiceTest {
@Mock
lateinit var network: Network
@Mock
lateinit var walledCheckCache: WalledCheckCache
@Mock
lateinit var networkCapabilities: NetworkCapabilities
@ -120,7 +122,7 @@ class ConnectivityServiceTest {
accountManager,
clientFactory,
requestBuilder,
Build.VERSION_CODES.Q
walledCheckCache
)
whenever(networkCapabilities.hasCapability(eq(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)))
@ -133,6 +135,7 @@ class ConnectivityServiceTest {
whenever(clientFactory.createPlainClient()).thenReturn(client)
whenever(user.server).thenReturn(newServer)
whenever(accountManager.user).thenReturn(user)
whenever(walledCheckCache.getValue()).thenReturn(null)
}
}