mirror of
https://github.com/nextcloud/android.git
synced 2024-11-24 06:05:42 +03:00
Merge master
Signed-off-by: alperozturk <alper_ozturk@proton.me>
This commit is contained in:
commit
15d2d91470
27 changed files with 1257 additions and 1215 deletions
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
|||
with:
|
||||
swap-size-gb: 10
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
|
||||
uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Set up JDK 17
|
||||
|
@ -46,4 +46,4 @@ jobs:
|
|||
echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties"
|
||||
./gradlew assembleDebug
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
|
||||
uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
|
||||
|
|
2
.github/workflows/scorecard.yml
vendored
2
.github/workflows/scorecard.yml
vendored
|
@ -37,6 +37,6 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
|
||||
uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
|
@ -84,8 +84,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 33
|
||||
compileSdk 33
|
||||
targetSdkVersion 34
|
||||
compileSdk 34
|
||||
|
||||
buildConfigField 'boolean', 'CI', ciBuild.toString()
|
||||
buildConfigField 'boolean', 'RUNTIME_PERF_ANALYSIS', perfAnalysis.toString()
|
||||
|
@ -246,7 +246,7 @@ dependencies {
|
|||
implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
implementation "androidx.appcompat:appcompat:$appCompatVersion"
|
||||
implementation 'androidx.webkit:webkit:1.7.0'
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 10 KiB |
|
@ -174,7 +174,7 @@ class ActivitiesActivityIT : AbstractIT() {
|
|||
sut.dismissSnackbar()
|
||||
}
|
||||
|
||||
shortSleep()
|
||||
longSleep()
|
||||
waitForIdleSync()
|
||||
|
||||
screenshot(sut)
|
||||
|
|
|
@ -92,7 +92,7 @@ abstract class NextcloudDatabase : RoomDatabase() {
|
|||
INSTANCE = Room
|
||||
.databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME)
|
||||
.allowMainThreadQueries()
|
||||
.addLegacyMigrations(clock)
|
||||
.addLegacyMigrations(clock, context)
|
||||
.addMigrations(RoomMigration())
|
||||
.addMigrations(Migration67to68())
|
||||
.addMigrations(Migration70to71())
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
package com.nextcloud.client.database.migrations
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
@ -36,12 +37,13 @@ private const val MIN_SUPPORTED_DB_VERSION = 24
|
|||
class LegacyMigration(
|
||||
private val from: Int,
|
||||
private val to: Int,
|
||||
private val clock: Clock
|
||||
private val clock: Clock,
|
||||
private val context: Context
|
||||
) : Migration(from, to) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
LegacyMigrationHelper(clock)
|
||||
.onUpgrade(database, from, to)
|
||||
LegacyMigrationHelper(clock, context)
|
||||
.tryUpgrade(database, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,10 +54,11 @@ class LegacyMigration(
|
|||
*/
|
||||
@Suppress("ForEachOnRange")
|
||||
fun RoomDatabase.Builder<NextcloudDatabase>.addLegacyMigrations(
|
||||
clock: Clock
|
||||
clock: Clock,
|
||||
context: Context
|
||||
): RoomDatabase.Builder<NextcloudDatabase> {
|
||||
(MIN_SUPPORTED_DB_VERSION until NextcloudDatabase.FIRST_ROOM_DB_VERSION - 1)
|
||||
.map { from -> LegacyMigration(from, from + 1, clock) }
|
||||
.map { from -> LegacyMigration(from, from + 1, clock, context) }
|
||||
.forEach { migration -> this.addMigrations(migration) }
|
||||
return this
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
package com.nextcloud.client.database.migrations;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
@ -31,11 +32,11 @@ import com.owncloud.android.datamodel.SyncedFolder;
|
|||
import com.owncloud.android.db.ProviderMeta;
|
||||
import com.owncloud.android.files.services.NameCollisionPolicy;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.providers.FileContentProvider;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
|
||||
public class LegacyMigrationHelper {
|
||||
|
||||
|
@ -52,12 +53,29 @@ public class LegacyMigrationHelper {
|
|||
private static final String UPGRADE_VERSION_MSG = "OUT of the ADD in onUpgrade; oldVersion == %d, newVersion == %d";
|
||||
|
||||
private final Clock clock;
|
||||
private final Context context;
|
||||
|
||||
public LegacyMigrationHelper(Clock clock) {
|
||||
public LegacyMigrationHelper(Clock clock, Context context) {
|
||||
this.clock = clock;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
public void tryUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
try {
|
||||
upgrade(db, oldVersion, newVersion);
|
||||
} catch (Throwable t) {
|
||||
Log_OC.i(TAG, "Migration upgrade failed due to " + t);
|
||||
clearStorage();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
|
||||
private void clearStorage() {
|
||||
context.getCacheDir().delete();
|
||||
((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).clearApplicationUserData();
|
||||
}
|
||||
|
||||
private void upgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
Log_OC.i(TAG, "Entering in onUpgrade");
|
||||
boolean upgraded = false;
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
@ -115,6 +116,8 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
|
|||
}
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -137,12 +140,14 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
|
|||
// not needed
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (getSupportFragmentManager().findFragmentByTag(BackupListFragment.TAG) != null) {
|
||||
getSupportFragmentManager().popBackStack(BACKUP_TO_LIST, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
} else {
|
||||
finish();
|
||||
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (getSupportFragmentManager().findFragmentByTag(BackupListFragment.TAG) != null) {
|
||||
getSupportFragmentManager().popBackStack(BACKUP_TO_LIST, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2507,8 +2507,7 @@ public class FileDisplayActivity extends FileActivity
|
|||
setUser(user);
|
||||
|
||||
if (fileId == null) {
|
||||
dismissLoadingDialog();
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
|
||||
onFileRequestError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2529,8 +2528,7 @@ public class FileDisplayActivity extends FileActivity
|
|||
setUser(user);
|
||||
|
||||
if (filepath == null) {
|
||||
dismissLoadingDialog();
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
|
||||
onFileRequestError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2544,8 +2542,7 @@ public class FileDisplayActivity extends FileActivity
|
|||
try {
|
||||
client = clientFactory.create(user);
|
||||
} catch (ClientFactory.CreationException e) {
|
||||
dismissLoadingDialog();
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
|
||||
onFileRequestError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2554,9 +2551,17 @@ public class FileDisplayActivity extends FileActivity
|
|||
client,
|
||||
storageManager,
|
||||
user);
|
||||
asyncRunner.postQuickTask(getRemoteFileTask, this::onFileRequestResult, null);
|
||||
asyncRunner.postQuickTask(getRemoteFileTask, this::onFileRequestResult, this::onFileRequestError);
|
||||
}
|
||||
|
||||
private Unit onFileRequestError(Throwable throwable) {
|
||||
dismissLoadingDialog();
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
|
||||
Log_OC.e(TAG, "Requesting file from remote failed!", throwable);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private Unit onFileRequestResult(GetRemoteFileTask.Result result) {
|
||||
dismissLoadingDialog();
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import android.view.ActionMode
|
|||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.nextcloud.client.di.Injectable
|
||||
|
@ -125,9 +126,33 @@ open class FolderPickerActivity :
|
|||
|
||||
// sets message for empty list of folders
|
||||
setBackgroundText()
|
||||
|
||||
handleOnBackPressed()
|
||||
|
||||
Log_OC.d(TAG, "onCreate() end")
|
||||
}
|
||||
|
||||
private fun handleOnBackPressed() {
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val listOfFiles = listOfFilesFragment
|
||||
if (listOfFiles != null) {
|
||||
// should never be null, indeed
|
||||
val levelsUp = listOfFiles.onBrowseUp()
|
||||
if (levelsUp == 0) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
file = listOfFiles.currentFile
|
||||
updateUiElements()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActionModeStarted(mode: ActionMode) {
|
||||
super.onActionModeStarted(mode)
|
||||
if (account != null) {
|
||||
|
@ -321,20 +346,6 @@ open class FolderPickerActivity :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val listOfFiles = listOfFilesFragment
|
||||
if (listOfFiles != null) {
|
||||
// should never be null, indeed
|
||||
val levelsUp = listOfFiles.onBrowseUp()
|
||||
if (levelsUp == 0) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
file = listOfFiles.currentFile
|
||||
updateUiElements()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUiElements() {
|
||||
toggleChooseEnabled()
|
||||
updateNavigationElementsInActionBar()
|
||||
|
|
|
@ -1,373 +0,0 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Andy Scherzinger
|
||||
* @author Mario Danic
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2017 Andy Scherzinger
|
||||
* Copyright (C) 2017 Mario Danic
|
||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) 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.owncloud.android.ui.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
import com.nextcloud.client.jobs.NotificationWork;
|
||||
import com.nextcloud.client.network.ClientFactory;
|
||||
import com.nextcloud.java.util.Optional;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.NotificationsLayoutBinding;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
|
||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation;
|
||||
import com.owncloud.android.lib.resources.notifications.models.Notification;
|
||||
import com.owncloud.android.ui.adapter.NotificationListAdapter;
|
||||
import com.owncloud.android.ui.asynctasks.DeleteAllNotificationsTask;
|
||||
import com.owncloud.android.ui.notifications.NotificationsContract;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.PushUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
/**
|
||||
* Activity displaying all server side stored notification items.
|
||||
*/
|
||||
public class NotificationsActivity extends DrawerActivity implements NotificationsContract.View {
|
||||
|
||||
private static final String TAG = NotificationsActivity.class.getSimpleName();
|
||||
|
||||
private NotificationsLayoutBinding binding;
|
||||
private NotificationListAdapter adapter;
|
||||
private Snackbar snackbar;
|
||||
private OwnCloudClient client;
|
||||
private Optional<User> optionalUser;
|
||||
|
||||
@Inject ClientFactory clientFactory;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
Log_OC.v(TAG, "onCreate() start");
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = NotificationsLayoutBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
optionalUser = getUser();
|
||||
|
||||
// use account from intent (opened via android notification can have a different account than current one)
|
||||
if (getIntent() != null && getIntent().getExtras() != null) {
|
||||
String accountName = getIntent().getExtras().getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT);
|
||||
if (accountName != null && optionalUser.isPresent()) {
|
||||
User user = optionalUser.get();
|
||||
if (user.getAccountName().equalsIgnoreCase(accountName)) {
|
||||
accountManager.setCurrentOwnCloudAccount(accountName);
|
||||
setUser(getUserAccountManager().getUser());
|
||||
optionalUser = getUser();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setup toolbar
|
||||
setupToolbar();
|
||||
|
||||
updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications));
|
||||
|
||||
viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList);
|
||||
viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingEmpty);
|
||||
|
||||
// setup drawer
|
||||
setupDrawer(R.id.nav_notifications);
|
||||
|
||||
if (!optionalUser.isPresent()) {
|
||||
// show error
|
||||
runOnUiThread(() -> setEmptyContent(
|
||||
getString(R.string.notifications_no_results_headline),
|
||||
getString(R.string.account_not_found))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
binding.swipeContainingList.setOnRefreshListener(() -> {
|
||||
setLoadingMessage();
|
||||
binding.swipeContainingList.setRefreshing(true);
|
||||
fetchAndSetData();
|
||||
});
|
||||
|
||||
binding.swipeContainingEmpty.setOnRefreshListener(() -> {
|
||||
setLoadingMessageEmpty();
|
||||
fetchAndSetData();
|
||||
});
|
||||
|
||||
setupPushWarning();
|
||||
setupContent();
|
||||
}
|
||||
|
||||
private void setupPushWarning() {
|
||||
if (!getResources().getBoolean(R.bool.show_push_warning)) {
|
||||
return;
|
||||
}
|
||||
if (snackbar != null) {
|
||||
if (!snackbar.isShown()) {
|
||||
snackbar.show();
|
||||
}
|
||||
} else {
|
||||
String pushUrl = getResources().getString(R.string.push_server_url);
|
||||
|
||||
if (pushUrl.isEmpty()) {
|
||||
snackbar = Snackbar.make(binding.emptyList.emptyListView,
|
||||
R.string.push_notifications_not_implemented,
|
||||
Snackbar.LENGTH_INDEFINITE);
|
||||
} else {
|
||||
final ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(this);
|
||||
final String accountName = optionalUser.isPresent() ? optionalUser.get().getAccountName() : "";
|
||||
final boolean usesOldLogin = arbitraryDataProvider.getBooleanValue(accountName,
|
||||
UserAccountManager.ACCOUNT_USES_STANDARD_PASSWORD);
|
||||
|
||||
if (usesOldLogin) {
|
||||
snackbar = Snackbar.make(binding.emptyList.emptyListView,
|
||||
R.string.push_notifications_old_login,
|
||||
Snackbar.LENGTH_INDEFINITE);
|
||||
} else {
|
||||
String pushValue = arbitraryDataProvider.getValue(accountName, PushUtils.KEY_PUSH);
|
||||
|
||||
if (pushValue == null || pushValue.isEmpty()) {
|
||||
snackbar = Snackbar.make(binding.emptyList.emptyListView,
|
||||
R.string.push_notifications_temp_error,
|
||||
Snackbar.LENGTH_INDEFINITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (snackbar != null && !snackbar.isShown()) {
|
||||
snackbar.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openDrawer() {
|
||||
super.openDrawer();
|
||||
|
||||
if (snackbar != null && snackbar.isShown()) {
|
||||
snackbar.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeDrawer() {
|
||||
super.closeDrawer();
|
||||
|
||||
setupPushWarning();
|
||||
}
|
||||
|
||||
/**
|
||||
* sets up the UI elements and loads all notification items.
|
||||
*/
|
||||
private void setupContent() {
|
||||
binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification);
|
||||
setLoadingMessageEmpty();
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
|
||||
binding.list.setLayoutManager(layoutManager);
|
||||
|
||||
fetchAndSetData();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void populateList(List<Notification> notifications) {
|
||||
initializeAdapter();
|
||||
adapter.setNotificationItems(notifications);
|
||||
binding.loadingContent.setVisibility(View.GONE);
|
||||
|
||||
if (notifications.size() > 0) {
|
||||
binding.swipeContainingEmpty.setVisibility(View.GONE);
|
||||
binding.swipeContainingList.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
setEmptyContent(
|
||||
getString(R.string.notifications_no_results_headline),
|
||||
getString(R.string.notifications_no_results_message)
|
||||
);
|
||||
binding.swipeContainingList.setVisibility(View.GONE);
|
||||
binding.swipeContainingEmpty.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchAndSetData() {
|
||||
Thread t = new Thread(() -> {
|
||||
initializeAdapter();
|
||||
|
||||
GetNotificationsRemoteOperation getRemoteNotificationOperation = new GetNotificationsRemoteOperation();
|
||||
final RemoteOperationResult<List<Notification>> result = getRemoteNotificationOperation.execute(client);
|
||||
|
||||
if (result.isSuccess() && result.getResultData() != null) {
|
||||
runOnUiThread(() -> populateList(result.getResultData()));
|
||||
} else {
|
||||
Log_OC.d(TAG, result.getLogMessage());
|
||||
// show error
|
||||
runOnUiThread(() -> setEmptyContent(getString(R.string.notifications_no_results_headline), result.getLogMessage()));
|
||||
}
|
||||
|
||||
hideRefreshLayoutLoader();
|
||||
});
|
||||
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void initializeClient() {
|
||||
if (client == null && optionalUser.isPresent()) {
|
||||
try {
|
||||
User user = optionalUser.get();
|
||||
client = clientFactory.create(user);
|
||||
} catch (ClientFactory.CreationException e) {
|
||||
Log_OC.e(TAG, "Error initializing client", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeAdapter() {
|
||||
initializeClient();
|
||||
if (adapter == null) {
|
||||
adapter = new NotificationListAdapter(client, this, viewThemeUtils);
|
||||
binding.list.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideRefreshLayoutLoader() {
|
||||
runOnUiThread(() -> {
|
||||
binding.swipeContainingList.setRefreshing(false);
|
||||
binding.swipeContainingEmpty.setRefreshing(false);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.activity_notifications, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
boolean retval = true;
|
||||
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
if (isDrawerOpen()) {
|
||||
closeDrawer();
|
||||
} else {
|
||||
openDrawer();
|
||||
}
|
||||
} else if (itemId == R.id.action_empty_notifications) {
|
||||
new DeleteAllNotificationsTask(client, this).execute();
|
||||
} else {
|
||||
retval = super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
private void setLoadingMessage() {
|
||||
binding.swipeContainingEmpty.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setLoadingMessageEmpty() {
|
||||
binding.swipeContainingList.setVisibility(View.GONE);
|
||||
binding.emptyList.emptyListView.setVisibility(View.GONE);
|
||||
binding.loadingContent.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setEmptyContent(String headline, String message) {
|
||||
binding.swipeContainingList.setVisibility(View.GONE);
|
||||
binding.loadingContent.setVisibility(View.GONE);
|
||||
binding.swipeContainingEmpty.setVisibility(View.VISIBLE);
|
||||
binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
|
||||
|
||||
binding.emptyList.emptyListViewHeadline.setText(headline);
|
||||
binding.emptyList.emptyListViewText.setText(message);
|
||||
binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification);
|
||||
|
||||
binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE);
|
||||
binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
setDrawerMenuItemChecked(R.id.nav_notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemovedNotification(boolean isSuccess) {
|
||||
if (!isSuccess) {
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.remove_notification_failed));
|
||||
fetchAndSetData();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNotification(NotificationListAdapter.NotificationViewHolder holder) {
|
||||
adapter.removeNotification(holder);
|
||||
|
||||
if (adapter.getItemCount() == 0) {
|
||||
setEmptyContent(getString(R.string.notifications_no_results_headline), getString(R.string.notifications_no_results_message));
|
||||
binding.swipeContainingList.setVisibility(View.GONE);
|
||||
binding.loadingContent.setVisibility(View.GONE);
|
||||
binding.swipeContainingEmpty.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemovedAllNotifications(boolean isSuccess) {
|
||||
if (isSuccess) {
|
||||
adapter.removeAllNotifications();
|
||||
setEmptyContent(getString(R.string.notifications_no_results_headline), getString(R.string.notifications_no_results_message));
|
||||
binding.loadingContent.setVisibility(View.GONE);
|
||||
binding.swipeContainingList.setVisibility(View.GONE);
|
||||
binding.swipeContainingEmpty.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.clear_notifications_failed));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionCallback(boolean isSuccess,
|
||||
Notification notification,
|
||||
NotificationListAdapter.NotificationViewHolder holder) {
|
||||
if (isSuccess) {
|
||||
adapter.removeNotification(holder);
|
||||
} else {
|
||||
adapter.setButtons(holder, notification);
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.notification_action_failed));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Andy Scherzinger
|
||||
* @author Mario Danic
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2017 Andy Scherzinger
|
||||
* Copyright (C) 2017 Mario Danic
|
||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) 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.owncloud.android.ui.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.jobs.NotificationWork
|
||||
import com.nextcloud.client.network.ClientFactory.CreationException
|
||||
import com.nextcloud.java.util.Optional
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.NotificationsLayoutBinding
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation
|
||||
import com.owncloud.android.lib.resources.notifications.models.Notification
|
||||
import com.owncloud.android.ui.adapter.NotificationListAdapter
|
||||
import com.owncloud.android.ui.adapter.NotificationListAdapter.NotificationViewHolder
|
||||
import com.owncloud.android.ui.asynctasks.DeleteAllNotificationsTask
|
||||
import com.owncloud.android.ui.notifications.NotificationsContract
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.PushUtils
|
||||
|
||||
/**
|
||||
* Activity displaying all server side stored notification items.
|
||||
*/
|
||||
class NotificationsActivity : DrawerActivity(), NotificationsContract.View {
|
||||
|
||||
private lateinit var binding: NotificationsLayoutBinding
|
||||
|
||||
private var adapter: NotificationListAdapter? = null
|
||||
private var snackbar: Snackbar? = null
|
||||
private var client: OwnCloudClient? = null
|
||||
private var optionalUser: Optional<User>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Log_OC.v(TAG, "onCreate() start")
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = NotificationsLayoutBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
optionalUser = user
|
||||
|
||||
intent?.let {
|
||||
it.extras?.let { bundle ->
|
||||
setupUser(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
setupToolbar()
|
||||
updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications))
|
||||
setupDrawer(R.id.nav_notifications)
|
||||
|
||||
if (optionalUser?.isPresent == false) {
|
||||
showError()
|
||||
}
|
||||
|
||||
setupContainingList()
|
||||
setupPushWarning()
|
||||
setupContent()
|
||||
}
|
||||
|
||||
private fun setupContainingList() {
|
||||
viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList)
|
||||
viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingEmpty)
|
||||
binding.swipeContainingList.setOnRefreshListener {
|
||||
setLoadingMessage()
|
||||
binding.swipeContainingList.isRefreshing = true
|
||||
fetchAndSetData()
|
||||
}
|
||||
binding.swipeContainingEmpty.setOnRefreshListener {
|
||||
setLoadingMessageEmpty()
|
||||
fetchAndSetData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUser(bundle: Bundle) {
|
||||
val accountName = bundle.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT)
|
||||
|
||||
if (accountName != null && optionalUser?.isPresent == true) {
|
||||
val user = optionalUser?.get()
|
||||
if (user?.accountName.equals(accountName, ignoreCase = true)) {
|
||||
accountManager.setCurrentOwnCloudAccount(accountName)
|
||||
setUser(userAccountManager.user)
|
||||
optionalUser = getUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError() {
|
||||
runOnUiThread {
|
||||
setEmptyContent(
|
||||
getString(R.string.notifications_no_results_headline),
|
||||
getString(R.string.account_not_found)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
private fun setupPushWarning() {
|
||||
if (!resources.getBoolean(R.bool.show_push_warning)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (snackbar != null) {
|
||||
if (snackbar?.isShown == false) {
|
||||
snackbar?.show()
|
||||
}
|
||||
} else {
|
||||
val pushUrl = resources.getString(R.string.push_server_url)
|
||||
if (pushUrl.isEmpty()) {
|
||||
snackbar = Snackbar.make(
|
||||
binding.emptyList.emptyListView,
|
||||
R.string.push_notifications_not_implemented,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
} else {
|
||||
val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(this)
|
||||
val accountName: String = if (optionalUser?.isPresent == true) {
|
||||
optionalUser?.get()?.accountName ?: ""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val usesOldLogin = arbitraryDataProvider.getBooleanValue(
|
||||
accountName,
|
||||
UserAccountManager.ACCOUNT_USES_STANDARD_PASSWORD
|
||||
)
|
||||
|
||||
if (usesOldLogin) {
|
||||
snackbar = Snackbar.make(
|
||||
binding.emptyList.emptyListView,
|
||||
R.string.push_notifications_old_login,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
} else {
|
||||
val pushValue = arbitraryDataProvider.getValue(accountName, PushUtils.KEY_PUSH)
|
||||
if (pushValue.isEmpty()) {
|
||||
snackbar = Snackbar.make(
|
||||
binding.emptyList.emptyListView,
|
||||
R.string.push_notifications_temp_error,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (snackbar != null && snackbar?.isShown == false) {
|
||||
snackbar?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun openDrawer() {
|
||||
super.openDrawer()
|
||||
if (snackbar != null && snackbar?.isShown == true) {
|
||||
snackbar?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun closeDrawer() {
|
||||
super.closeDrawer()
|
||||
setupPushWarning()
|
||||
}
|
||||
|
||||
/**
|
||||
* sets up the UI elements and loads all notification items.
|
||||
*/
|
||||
private fun setupContent() {
|
||||
binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification)
|
||||
setLoadingMessageEmpty()
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
binding.list.layoutManager = layoutManager
|
||||
fetchAndSetData()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun populateList(notifications: List<Notification>?) {
|
||||
initializeAdapter()
|
||||
adapter?.setNotificationItems(notifications)
|
||||
binding.loadingContent.visibility = View.GONE
|
||||
|
||||
if (notifications?.isNotEmpty() == true) {
|
||||
binding.swipeContainingEmpty.visibility = View.GONE
|
||||
binding.swipeContainingList.visibility = View.VISIBLE
|
||||
} else {
|
||||
setEmptyContent(
|
||||
getString(R.string.notifications_no_results_headline),
|
||||
getString(R.string.notifications_no_results_message)
|
||||
)
|
||||
binding.swipeContainingList.visibility = View.GONE
|
||||
binding.swipeContainingEmpty.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchAndSetData() {
|
||||
val t = Thread {
|
||||
initializeAdapter()
|
||||
val getRemoteNotificationOperation = GetNotificationsRemoteOperation()
|
||||
val result = getRemoteNotificationOperation.execute(client)
|
||||
if (result.isSuccess && result.resultData != null) {
|
||||
runOnUiThread { populateList(result.resultData) }
|
||||
} else {
|
||||
Log_OC.d(TAG, result.logMessage)
|
||||
// show error
|
||||
runOnUiThread {
|
||||
setEmptyContent(
|
||||
getString(R.string.notifications_no_results_headline),
|
||||
result.logMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
hideRefreshLayoutLoader()
|
||||
}
|
||||
t.start()
|
||||
}
|
||||
|
||||
private fun initializeClient() {
|
||||
if (client == null && optionalUser?.isPresent == true) {
|
||||
try {
|
||||
val user = optionalUser?.get()
|
||||
client = clientFactory.create(user)
|
||||
} catch (e: CreationException) {
|
||||
Log_OC.e(TAG, "Error initializing client", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeAdapter() {
|
||||
initializeClient()
|
||||
if (adapter == null) {
|
||||
adapter = NotificationListAdapter(client, this, viewThemeUtils)
|
||||
binding.list.adapter = adapter
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideRefreshLayoutLoader() {
|
||||
runOnUiThread {
|
||||
binding.swipeContainingList.isRefreshing = false
|
||||
binding.swipeContainingEmpty.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_notifications, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
var retval = true
|
||||
val itemId = item.itemId
|
||||
if (itemId == android.R.id.home) {
|
||||
if (isDrawerOpen) {
|
||||
closeDrawer()
|
||||
} else {
|
||||
openDrawer()
|
||||
}
|
||||
} else if (itemId == R.id.action_empty_notifications) {
|
||||
DeleteAllNotificationsTask(client, this).execute()
|
||||
} else {
|
||||
retval = super.onOptionsItemSelected(item)
|
||||
}
|
||||
return retval
|
||||
}
|
||||
|
||||
private fun setLoadingMessage() {
|
||||
binding.swipeContainingEmpty.visibility = View.GONE
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun setLoadingMessageEmpty() {
|
||||
binding.swipeContainingList.visibility = View.GONE
|
||||
binding.emptyList.emptyListView.visibility = View.GONE
|
||||
binding.loadingContent.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun setEmptyContent(headline: String?, message: String?) {
|
||||
binding.swipeContainingList.visibility = View.GONE
|
||||
binding.loadingContent.visibility = View.GONE
|
||||
binding.swipeContainingEmpty.visibility = View.VISIBLE
|
||||
binding.emptyList.emptyListView.visibility = View.VISIBLE
|
||||
binding.emptyList.emptyListViewHeadline.text = headline
|
||||
binding.emptyList.emptyListViewText.text = message
|
||||
binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification)
|
||||
binding.emptyList.emptyListViewText.visibility = View.VISIBLE
|
||||
binding.emptyList.emptyListIcon.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setDrawerMenuItemChecked(R.id.nav_notifications)
|
||||
}
|
||||
|
||||
override fun onRemovedNotification(isSuccess: Boolean) {
|
||||
if (!isSuccess) {
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.remove_notification_failed))
|
||||
fetchAndSetData()
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeNotification(holder: NotificationViewHolder) {
|
||||
adapter?.removeNotification(holder)
|
||||
if (adapter?.itemCount == 0) {
|
||||
setEmptyContent(
|
||||
getString(R.string.notifications_no_results_headline),
|
||||
getString(R.string.notifications_no_results_message)
|
||||
)
|
||||
binding.swipeContainingList.visibility = View.GONE
|
||||
binding.loadingContent.visibility = View.GONE
|
||||
binding.swipeContainingEmpty.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemovedAllNotifications(isSuccess: Boolean) {
|
||||
if (isSuccess) {
|
||||
adapter?.removeAllNotifications()
|
||||
setEmptyContent(
|
||||
getString(R.string.notifications_no_results_headline),
|
||||
getString(R.string.notifications_no_results_message)
|
||||
)
|
||||
binding.loadingContent.visibility = View.GONE
|
||||
binding.swipeContainingList.visibility = View.GONE
|
||||
binding.swipeContainingEmpty.visibility = View.VISIBLE
|
||||
} else {
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.clear_notifications_failed))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionCallback(
|
||||
isSuccess: Boolean,
|
||||
notification: Notification,
|
||||
holder: NotificationViewHolder
|
||||
) {
|
||||
if (isSuccess) {
|
||||
adapter?.removeNotification(holder)
|
||||
} else {
|
||||
adapter?.setButtons(holder, notification)
|
||||
DisplayUtils.showSnackMessage(this, getString(R.string.notification_action_failed))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = NotificationsActivity::class.java.simpleName
|
||||
}
|
||||
}
|
|
@ -1,478 +0,0 @@
|
|||
/*
|
||||
* ownCloud Android client application
|
||||
*
|
||||
* @author Bartek Przybylski
|
||||
* @author masensio
|
||||
* @author David A. Velasco
|
||||
* Copyright (C) 2011 Bartek Przybylski
|
||||
* Copyright (C) 2015 ownCloud Inc.
|
||||
* Copyright (C) 2020 Kwon Yuna <yunaghgh@naver.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.owncloud.android.ui.activity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.nextcloud.client.di.Injectable;
|
||||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.authentication.PassCodeManager;
|
||||
import com.owncloud.android.databinding.PasscodelockBinding;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.ui.components.PassCodeEditText;
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
public class PassCodeActivity extends AppCompatActivity implements Injectable {
|
||||
|
||||
private static final String TAG = PassCodeActivity.class.getSimpleName();
|
||||
private static final String KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS";
|
||||
private static final String KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE";
|
||||
|
||||
public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT";
|
||||
public final static String ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT";
|
||||
public final static String ACTION_CHECK = "ACTION_CHECK";
|
||||
public final static String KEY_PASSCODE = "KEY_PASSCODE";
|
||||
public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
|
||||
|
||||
public final static String PREFERENCE_PASSCODE_D = "PrefPinCode";
|
||||
public final static String PREFERENCE_PASSCODE_D1 = "PrefPinCode1";
|
||||
public final static String PREFERENCE_PASSCODE_D2 = "PrefPinCode2";
|
||||
public final static String PREFERENCE_PASSCODE_D3 = "PrefPinCode3";
|
||||
public final static String PREFERENCE_PASSCODE_D4 = "PrefPinCode4";
|
||||
|
||||
@Inject AppPreferences preferences;
|
||||
@Inject PassCodeManager passCodeManager;
|
||||
@Inject ViewThemeUtils viewThemeUtils;
|
||||
private PasscodelockBinding binding;
|
||||
private final PassCodeEditText[] passCodeEditTexts = new PassCodeEditText[4];
|
||||
private String[] passCodeDigits = {"", "", "", ""};
|
||||
private boolean confirmingPassCode;
|
||||
private boolean changed = true; // to control that only one blocks jump
|
||||
|
||||
/**
|
||||
* Initializes the activity.
|
||||
* <p>
|
||||
* An intent with a valid ACTION is expected; if none is found, an {@link IllegalArgumentException} will be thrown.
|
||||
*
|
||||
* @param savedInstanceState Previously saved state - irrelevant in this case
|
||||
*/
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = PasscodelockBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
viewThemeUtils.platform.colorTextButtons(binding.cancel);
|
||||
|
||||
passCodeEditTexts[0] = binding.txt0;
|
||||
passCodeEditTexts[1] = binding.txt1;
|
||||
passCodeEditTexts[2] = binding.txt2;
|
||||
passCodeEditTexts[3] = binding.txt3;
|
||||
|
||||
for (EditText passCodeEditText : passCodeEditTexts) {
|
||||
viewThemeUtils.platform.colorEditText(passCodeEditText);
|
||||
}
|
||||
|
||||
passCodeEditTexts[0].requestFocus();
|
||||
|
||||
Window window = getWindow();
|
||||
if (window != null) {
|
||||
window.setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
}
|
||||
|
||||
if (ACTION_CHECK.equals(getIntent().getAction())) {
|
||||
/// this is a pass code request; the user has to input the right value
|
||||
binding.header.setText(R.string.pass_code_enter_pass_code);
|
||||
binding.explanation.setVisibility(View.INVISIBLE);
|
||||
setCancelButtonEnabled(false); // no option to cancel
|
||||
|
||||
showDelay();
|
||||
|
||||
} else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
|
||||
if (savedInstanceState != null) {
|
||||
confirmingPassCode = savedInstanceState.getBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE);
|
||||
passCodeDigits = savedInstanceState.getStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS);
|
||||
}
|
||||
if (confirmingPassCode) {
|
||||
// the app was in the passcode confirmation
|
||||
requestPassCodeConfirmation();
|
||||
} else {
|
||||
// pass code preference has just been activated in SettingsActivity;
|
||||
// will receive and confirm pass code value
|
||||
binding.header.setText(R.string.pass_code_configure_your_pass_code);
|
||||
|
||||
binding.explanation.setVisibility(View.VISIBLE);
|
||||
}
|
||||
setCancelButtonEnabled(true);
|
||||
|
||||
} else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
|
||||
// pass code preference has just been disabled in SettingsActivity;
|
||||
// will confirm user knows pass code, then remove it
|
||||
binding.header.setText(R.string.pass_code_remove_your_pass_code);
|
||||
binding.explanation.setVisibility(View.INVISIBLE);
|
||||
setCancelButtonEnabled(true);
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to " + TAG);
|
||||
}
|
||||
|
||||
setTextListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the cancel button to allow the user interrupt the ACTION requested to the activity.
|
||||
*
|
||||
* @param enabled 'True' makes the cancel button available, 'false' hides it.
|
||||
*/
|
||||
protected void setCancelButtonEnabled(boolean enabled) {
|
||||
if (enabled) {
|
||||
binding.cancel.setVisibility(View.VISIBLE);
|
||||
binding.cancel.setOnClickListener(v -> finish());
|
||||
} else {
|
||||
binding.cancel.setVisibility(View.INVISIBLE);
|
||||
binding.cancel.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public PasscodelockBinding getBinding() {
|
||||
return binding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the appropriate listeners to the input boxes receiving each digit of the pass code.
|
||||
*/
|
||||
protected void setTextListeners() {
|
||||
for (int i = 0; i < passCodeEditTexts.length; i++) {
|
||||
final PassCodeEditText editText = passCodeEditTexts[i];
|
||||
boolean isLast = (i == 3);
|
||||
|
||||
editText.addTextChangedListener(new PassCodeDigitTextWatcher(i, isLast));
|
||||
if (i > 0) {
|
||||
setOnKeyListener(i);
|
||||
}
|
||||
|
||||
int finalIndex = i;
|
||||
editText.setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(finalIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private void onPassCodeEditTextFocusChange(final int passCodeIndex) {
|
||||
for (int i = 0; i < passCodeIndex; i++) {
|
||||
if (TextUtils.isEmpty(passCodeEditTexts[i].getText())) {
|
||||
passCodeEditTexts[i].requestFocus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setOnKeyListener(final int passCodeIndex) {
|
||||
passCodeEditTexts[passCodeIndex].setOnKeyListener((v, keyCode, event) -> {
|
||||
if (keyCode == KeyEvent.KEYCODE_DEL && changed) {
|
||||
passCodeEditTexts[passCodeIndex - 1].requestFocus();
|
||||
if (!confirmingPassCode) {
|
||||
passCodeDigits[passCodeIndex - 1] = "";
|
||||
}
|
||||
passCodeEditTexts[passCodeIndex - 1].setText("");
|
||||
changed = false;
|
||||
|
||||
} else if (!changed) {
|
||||
changed = true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the pass code entered by the user just after the last digit was in.
|
||||
* <p>
|
||||
* Takes into account the action requested to the activity, the currently saved pass code and the previously typed
|
||||
* pass code, if any.
|
||||
*/
|
||||
private void processFullPassCode() {
|
||||
if (ACTION_CHECK.equals(getIntent().getAction())) {
|
||||
if (checkPassCode()) {
|
||||
preferences.resetPinWrongAttempts();
|
||||
|
||||
/// pass code accepted in request, user is allowed to access the app
|
||||
passCodeManager.updateLockTimestamp();
|
||||
hideSoftKeyboard();
|
||||
finish();
|
||||
|
||||
} else {
|
||||
preferences.increasePinWrongAttempts();
|
||||
|
||||
showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE);
|
||||
}
|
||||
|
||||
} else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
|
||||
if (checkPassCode()) {
|
||||
passCodeManager.updateLockTimestamp();
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(KEY_CHECK_RESULT, true);
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
hideSoftKeyboard();
|
||||
finish();
|
||||
} else {
|
||||
showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE);
|
||||
}
|
||||
|
||||
} else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
|
||||
/// enabling pass code
|
||||
if (!confirmingPassCode) {
|
||||
requestPassCodeConfirmation();
|
||||
|
||||
} else if (confirmPassCode()) {
|
||||
/// confirmed: user typed the same pass code twice
|
||||
savePassCodeAndExit();
|
||||
|
||||
} else {
|
||||
showErrorAndRestart(R.string.pass_code_mismatch, R.string.pass_code_configure_your_pass_code, View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSoftKeyboard() {
|
||||
View focusedView = getCurrentFocus();
|
||||
if (focusedView != null) {
|
||||
InputMethodManager inputMethodManager =
|
||||
(InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(
|
||||
focusedView.getWindowToken(),
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
private void showErrorAndRestart(int errorMessage, int headerMessage, int explanationVisibility) {
|
||||
Arrays.fill(passCodeDigits, null);
|
||||
Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show();
|
||||
binding.header.setText(headerMessage); // TODO check if really needed
|
||||
binding.explanation.setVisibility(explanationVisibility); // TODO check if really needed
|
||||
clearBoxes();
|
||||
|
||||
showDelay();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ask to the user for retyping the pass code just entered before saving it as the current pass code.
|
||||
*/
|
||||
protected void requestPassCodeConfirmation() {
|
||||
clearBoxes();
|
||||
binding.header.setText(R.string.pass_code_reenter_your_pass_code);
|
||||
binding.explanation.setVisibility(View.INVISIBLE);
|
||||
confirmingPassCode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares pass code entered by the user with the value currently saved in the app.
|
||||
*
|
||||
* @return 'True' if entered pass code equals to the saved one.
|
||||
*/
|
||||
protected boolean checkPassCode() {
|
||||
String[] savedPassCodeDigits = preferences.getPassCode();
|
||||
|
||||
boolean result = true;
|
||||
for (int i = 0; i < passCodeDigits.length && result; i++) {
|
||||
result = passCodeDigits[i] != null && passCodeDigits[i].equals(savedPassCodeDigits[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares pass code retyped by the user in the input fields with the value entered just before.
|
||||
*
|
||||
* @return 'True' if retyped pass code equals to the entered before.
|
||||
*/
|
||||
protected boolean confirmPassCode() {
|
||||
confirmingPassCode = false;
|
||||
|
||||
for (int i = 0; i < passCodeEditTexts.length; i++) {
|
||||
Editable passCodeText = passCodeEditTexts[i].getText();
|
||||
if (passCodeText == null || !passCodeText.toString().equals(passCodeDigits[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input fields to empty strings and puts the focus on the first one.
|
||||
*/
|
||||
protected void clearBoxes() {
|
||||
for (EditText mPassCodeEditText : passCodeEditTexts) {
|
||||
mPassCodeEditText.setText("");
|
||||
}
|
||||
passCodeEditTexts[0].requestFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing than
|
||||
* ACTION_CHECK may be worked around.
|
||||
*
|
||||
* @param keyCode Key code of the key that triggered the down event.
|
||||
* @param event Event triggered.
|
||||
* @return 'True' when the key event was processed by this method.
|
||||
*/
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
|
||||
if (ACTION_CHECK.equals(getIntent().getAction())) {
|
||||
moveTaskToBack(true);
|
||||
finishAndRemoveTask();
|
||||
} else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction()) ||
|
||||
ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
|
||||
finish();
|
||||
}// else, do nothing, but report that the key was consumed to stay alive
|
||||
return true;
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the pass code input by the user as the current pass code.
|
||||
*/
|
||||
protected void savePassCodeAndExit() {
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(KEY_PASSCODE,
|
||||
passCodeDigits[0] + passCodeDigits[1] + passCodeDigits[2] + passCodeDigits[3]);
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
|
||||
passCodeManager.updateLockTimestamp();
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void showDelay() {
|
||||
int delay = preferences.pinBruteForceDelay();
|
||||
|
||||
if (delay > 0) {
|
||||
binding.explanation.setText(R.string.brute_force_delay);
|
||||
binding.explanation.setVisibility(View.VISIBLE);
|
||||
binding.txt0.setEnabled(false);
|
||||
binding.txt1.setEnabled(false);
|
||||
binding.txt2.setEnabled(false);
|
||||
binding.txt3.setEnabled(false);
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(delay * 1000L);
|
||||
|
||||
runOnUiThread(() -> {
|
||||
binding.explanation.setVisibility(View.INVISIBLE);
|
||||
binding.txt0.setEnabled(true);
|
||||
binding.txt1.setEnabled(true);
|
||||
binding.txt2.setEnabled(true);
|
||||
binding.txt3.setEnabled(true);
|
||||
});
|
||||
} catch (InterruptedException e) {
|
||||
Log_OC.e(this, "Could not delay password input prompt");
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE, confirmingPassCode);
|
||||
outState.putStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS, passCodeDigits);
|
||||
}
|
||||
|
||||
private class PassCodeDigitTextWatcher implements TextWatcher {
|
||||
|
||||
private int mIndex = -1;
|
||||
private boolean mLastOne;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param index Position in the pass code of the input field that will be bound to this watcher.
|
||||
* @param lastOne 'True' means that watcher corresponds to the last position of the pass code.
|
||||
*/
|
||||
PassCodeDigitTextWatcher(int index, boolean lastOne) {
|
||||
mIndex = index;
|
||||
mLastOne = lastOne;
|
||||
|
||||
if (mIndex < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid index in " + PassCodeDigitTextWatcher.class.getSimpleName() +
|
||||
" constructor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private int next() {
|
||||
return mLastOne ? 0 : mIndex + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs several actions when the user types a digit in an input field: - saves the input digit to the state
|
||||
* of the activity; this will allow retyping the pass code to confirm it. - moves the focus automatically to the
|
||||
* next field - for the last field, triggers the processing of the full pass code
|
||||
*
|
||||
* @param s Changed text
|
||||
*/
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s.length() > 0) {
|
||||
if (!confirmingPassCode) {
|
||||
Editable passCodeText = passCodeEditTexts[mIndex].getText();
|
||||
|
||||
if (passCodeText != null) {
|
||||
passCodeDigits[mIndex] = passCodeText.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (mLastOne) {
|
||||
processFullPassCode();
|
||||
} else {
|
||||
passCodeEditTexts[next()].requestFocus();
|
||||
}
|
||||
|
||||
} else {
|
||||
Log_OC.d(TAG, "Text box " + mIndex + " was cleaned");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,437 @@
|
|||
/*
|
||||
* ownCloud Android client application
|
||||
*
|
||||
* @author Bartek Przybylski
|
||||
* @author masensio
|
||||
* @author David A. Velasco
|
||||
* Copyright (C) 2011 Bartek Przybylski
|
||||
* Copyright (C) 2015 ownCloud Inc.
|
||||
* Copyright (C) 2020 Kwon Yuna <yunaghgh@naver.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.owncloud.android.ui.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.authentication.PassCodeManager
|
||||
import com.owncloud.android.databinding.PasscodelockBinding
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.ui.components.PassCodeEditText
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import java.util.Arrays
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("TooManyFunctions", "MagicNumber")
|
||||
class PassCodeActivity : AppCompatActivity(), Injectable {
|
||||
|
||||
companion object {
|
||||
private val TAG = PassCodeActivity::class.java.simpleName
|
||||
|
||||
private const val KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS"
|
||||
private const val KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE"
|
||||
const val ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT"
|
||||
const val ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT"
|
||||
const val ACTION_CHECK = "ACTION_CHECK"
|
||||
const val KEY_PASSCODE = "KEY_PASSCODE"
|
||||
const val KEY_CHECK_RESULT = "KEY_CHECK_RESULT"
|
||||
const val PREFERENCE_PASSCODE_D = "PrefPinCode"
|
||||
const val PREFERENCE_PASSCODE_D1 = "PrefPinCode1"
|
||||
const val PREFERENCE_PASSCODE_D2 = "PrefPinCode2"
|
||||
const val PREFERENCE_PASSCODE_D3 = "PrefPinCode3"
|
||||
const val PREFERENCE_PASSCODE_D4 = "PrefPinCode4"
|
||||
}
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var preferences: AppPreferences? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var passCodeManager: PassCodeManager? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var viewThemeUtils: ViewThemeUtils? = null
|
||||
|
||||
@get:VisibleForTesting
|
||||
lateinit var binding: PasscodelockBinding
|
||||
private set
|
||||
|
||||
private val passCodeEditTexts = arrayOfNulls<PassCodeEditText>(4)
|
||||
private var passCodeDigits: Array<String?>? = arrayOf("", "", "", "")
|
||||
private var confirmingPassCode = false
|
||||
private var changed = true // to control that only one blocks jump
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = PasscodelockBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
applyTint()
|
||||
setupPasscodeEditTexts()
|
||||
setSoftInputMode()
|
||||
setupUI(savedInstanceState)
|
||||
setTextListeners()
|
||||
}
|
||||
|
||||
private fun applyTint() {
|
||||
viewThemeUtils?.platform?.colorViewBackground(binding.cardViewContent, ColorRole.SURFACE_VARIANT)
|
||||
viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(binding.cancel)
|
||||
}
|
||||
|
||||
private fun setupPasscodeEditTexts() {
|
||||
passCodeEditTexts[0] = binding.txt0
|
||||
passCodeEditTexts[1] = binding.txt1
|
||||
passCodeEditTexts[2] = binding.txt2
|
||||
passCodeEditTexts[3] = binding.txt3
|
||||
|
||||
passCodeEditTexts.forEach {
|
||||
it?.let { viewThemeUtils?.platform?.colorEditText(it) }
|
||||
}
|
||||
|
||||
passCodeEditTexts[0]?.requestFocus()
|
||||
}
|
||||
|
||||
private fun setSoftInputMode() {
|
||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
|
||||
}
|
||||
|
||||
private fun setupUI(savedInstanceState: Bundle?) {
|
||||
if (ACTION_CHECK == intent.action) {
|
||||
// / this is a pass code request; the user has to input the right value
|
||||
binding.header.setText(R.string.pass_code_enter_pass_code)
|
||||
binding.explanation.visibility = View.INVISIBLE
|
||||
setCancelButtonEnabled(false) // no option to cancel
|
||||
showDelay()
|
||||
} else if (ACTION_REQUEST_WITH_RESULT == intent.action) {
|
||||
if (savedInstanceState != null) {
|
||||
confirmingPassCode = savedInstanceState.getBoolean(KEY_CONFIRMING_PASSCODE)
|
||||
passCodeDigits = savedInstanceState.getStringArray(KEY_PASSCODE_DIGITS)
|
||||
}
|
||||
if (confirmingPassCode) {
|
||||
// the app was in the passcode confirmation
|
||||
requestPassCodeConfirmation()
|
||||
} else {
|
||||
// pass code preference has just been activated in SettingsActivity;
|
||||
// will receive and confirm pass code value
|
||||
binding.header.setText(R.string.pass_code_configure_your_pass_code)
|
||||
binding.explanation.visibility = View.VISIBLE
|
||||
}
|
||||
setCancelButtonEnabled(true)
|
||||
} else if (ACTION_CHECK_WITH_RESULT == intent.action) {
|
||||
// pass code preference has just been disabled in SettingsActivity;
|
||||
// will confirm user knows pass code, then remove it
|
||||
binding.header.setText(R.string.pass_code_remove_your_pass_code)
|
||||
binding.explanation.visibility = View.INVISIBLE
|
||||
setCancelButtonEnabled(true)
|
||||
} else {
|
||||
throw IllegalArgumentException("A valid ACTION is needed in the Intent passed to $TAG")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCancelButtonEnabled(enabled: Boolean) {
|
||||
binding.cancel.visibility = if (enabled) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.INVISIBLE
|
||||
}
|
||||
binding.cancel.setOnClickListener {
|
||||
if (enabled) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTextListeners() {
|
||||
for (i in passCodeEditTexts.indices) {
|
||||
val editText = passCodeEditTexts[i]
|
||||
val isLast = (i == 3)
|
||||
|
||||
editText?.addTextChangedListener(PassCodeDigitTextWatcher(i, isLast))
|
||||
|
||||
if (i > 0) {
|
||||
setOnKeyListener(i)
|
||||
}
|
||||
|
||||
editText?.onFocusChangeListener = View.OnFocusChangeListener { _: View?, _: Boolean ->
|
||||
onPassCodeEditTextFocusChange(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPassCodeEditTextFocusChange(passCodeIndex: Int) {
|
||||
for (i in 0 until passCodeIndex) {
|
||||
if (TextUtils.isEmpty(passCodeEditTexts[i]?.text)) {
|
||||
passCodeEditTexts[i]?.requestFocus()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOnKeyListener(passCodeIndex: Int) {
|
||||
passCodeEditTexts[passCodeIndex]?.setOnKeyListener { _: View?, keyCode: Int, _: KeyEvent? ->
|
||||
if (keyCode == KeyEvent.KEYCODE_DEL && changed) {
|
||||
passCodeEditTexts[passCodeIndex - 1]?.requestFocus()
|
||||
|
||||
if (!confirmingPassCode) {
|
||||
passCodeDigits?.set(passCodeIndex - 1, "")
|
||||
}
|
||||
|
||||
passCodeEditTexts[passCodeIndex - 1]?.setText("")
|
||||
|
||||
changed = false
|
||||
} else if (!changed) {
|
||||
changed = true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the pass code entered by the user just after the last digit was in.
|
||||
*
|
||||
*
|
||||
* Takes into account the action requested to the activity, the currently saved pass code and the previously typed
|
||||
* pass code, if any.
|
||||
*/
|
||||
private fun processFullPassCode() {
|
||||
if (ACTION_CHECK == intent.action) {
|
||||
if (checkPassCode()) {
|
||||
preferences?.resetPinWrongAttempts()
|
||||
|
||||
// / pass code accepted in request, user is allowed to access the app
|
||||
passCodeManager?.updateLockTimestamp()
|
||||
hideSoftKeyboard()
|
||||
finish()
|
||||
} else {
|
||||
preferences?.increasePinWrongAttempts()
|
||||
showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE)
|
||||
}
|
||||
} else if (ACTION_CHECK_WITH_RESULT == intent.action) {
|
||||
if (checkPassCode()) {
|
||||
passCodeManager?.updateLockTimestamp()
|
||||
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra(KEY_CHECK_RESULT, true)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
hideSoftKeyboard()
|
||||
finish()
|
||||
} else {
|
||||
showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE)
|
||||
}
|
||||
} else if (ACTION_REQUEST_WITH_RESULT == intent.action) {
|
||||
// / enabling pass code
|
||||
if (!confirmingPassCode) {
|
||||
requestPassCodeConfirmation()
|
||||
} else if (confirmPassCode()) {
|
||||
// / confirmed: user typed the same pass code twice
|
||||
savePassCodeAndExit()
|
||||
} else {
|
||||
showErrorAndRestart(
|
||||
R.string.pass_code_mismatch,
|
||||
R.string.pass_code_configure_your_pass_code,
|
||||
View.VISIBLE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSoftKeyboard() {
|
||||
currentFocus?.let {
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(
|
||||
it.windowToken,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorAndRestart(errorMessage: Int, headerMessage: Int, explanationVisibility: Int) {
|
||||
passCodeDigits?.let { Arrays.fill(it, null) }
|
||||
|
||||
Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show()
|
||||
binding.header.setText(headerMessage) // TODO check if really needed
|
||||
binding.explanation.visibility = explanationVisibility // TODO check if really needed
|
||||
clearBoxes()
|
||||
showDelay()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask to the user for retyping the pass code just entered before saving it as the current pass code.
|
||||
*/
|
||||
private fun requestPassCodeConfirmation() {
|
||||
clearBoxes()
|
||||
binding.header.setText(R.string.pass_code_reenter_your_pass_code)
|
||||
binding.explanation.visibility = View.INVISIBLE
|
||||
confirmingPassCode = true
|
||||
}
|
||||
|
||||
private fun checkPassCode(): Boolean {
|
||||
val savedPassCodeDigits = preferences?.passCode
|
||||
return passCodeDigits?.zip(savedPassCodeDigits.orEmpty()) { input, saved ->
|
||||
input != null && input == saved
|
||||
}?.all { it } ?: false
|
||||
}
|
||||
|
||||
private fun confirmPassCode(): Boolean {
|
||||
return passCodeEditTexts.indices.all { i ->
|
||||
passCodeEditTexts[i]?.text.toString() == passCodeDigits!![i]
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearBoxes() {
|
||||
passCodeEditTexts.forEach { it?.text?.clear() }
|
||||
passCodeEditTexts.firstOrNull()?.requestFocus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing than
|
||||
* ACTION_CHECK may be worked around.
|
||||
*
|
||||
* @param keyCode Key code of the key that triggered the down event.
|
||||
* @param event Event triggered.
|
||||
* @return 'True' when the key event was processed by this method.
|
||||
*/
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) {
|
||||
if (ACTION_CHECK == intent.action) {
|
||||
moveTaskToBack(true)
|
||||
finishAndRemoveTask()
|
||||
} else if (ACTION_REQUEST_WITH_RESULT == intent.action || ACTION_CHECK_WITH_RESULT == intent.action) {
|
||||
finish()
|
||||
} // else, do nothing, but report that the key was consumed to stay alive
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
private fun savePassCodeAndExit() {
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra(
|
||||
KEY_PASSCODE,
|
||||
passCodeDigits!![0] + passCodeDigits!![1] + passCodeDigits!![2] + passCodeDigits!![3]
|
||||
)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
passCodeManager?.updateLockTimestamp()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showDelay() {
|
||||
val delay = preferences?.pinBruteForceDelay() ?: 0
|
||||
|
||||
if (delay <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
binding.explanation.setText(R.string.brute_force_delay)
|
||||
binding.explanation.visibility = View.VISIBLE
|
||||
binding.txt0.isEnabled = false
|
||||
binding.txt1.isEnabled = false
|
||||
binding.txt2.isEnabled = false
|
||||
binding.txt3.isEnabled = false
|
||||
|
||||
Thread(object : Runnable {
|
||||
override fun run() {
|
||||
try {
|
||||
Thread.sleep(delay * 1000L)
|
||||
|
||||
runOnUiThread {
|
||||
binding.explanation.visibility = View.INVISIBLE
|
||||
binding.txt0.isEnabled = true
|
||||
binding.txt1.isEnabled = true
|
||||
binding.txt2.isEnabled = true
|
||||
binding.txt3.isEnabled = true
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log_OC.e(this, "Could not delay password input prompt")
|
||||
}
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
public override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(KEY_CONFIRMING_PASSCODE, confirmingPassCode)
|
||||
outState.putStringArray(KEY_PASSCODE_DIGITS, passCodeDigits)
|
||||
}
|
||||
|
||||
private inner class PassCodeDigitTextWatcher(index: Int, lastOne: Boolean) : TextWatcher {
|
||||
private var mIndex = -1
|
||||
private val mLastOne: Boolean
|
||||
|
||||
init {
|
||||
mIndex = index
|
||||
mLastOne = lastOne
|
||||
|
||||
require(mIndex >= 0) {
|
||||
"Invalid index in " + PassCodeDigitTextWatcher::class.java.simpleName +
|
||||
" constructor"
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun next(): Int {
|
||||
return if (mLastOne) 0 else mIndex + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs several actions when the user types a digit in an input field: - saves the input digit to the state
|
||||
* of the activity; this will allow retyping the pass code to confirm it. - moves the focus automatically to the
|
||||
* next field - for the last field, triggers the processing of the full pass code
|
||||
*
|
||||
* @param s Changed text
|
||||
*/
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
if (s.isNotEmpty()) {
|
||||
if (!confirmingPassCode) {
|
||||
val passCodeText = passCodeEditTexts[mIndex]?.text
|
||||
|
||||
if (passCodeText != null) {
|
||||
passCodeDigits!![mIndex] = passCodeText.toString()
|
||||
}
|
||||
}
|
||||
|
||||
if (mLastOne) {
|
||||
processFullPassCode()
|
||||
} else {
|
||||
passCodeEditTexts[next()]?.requestFocus()
|
||||
}
|
||||
} else {
|
||||
Log_OC.d(TAG, "Text box $mIndex was cleaned")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("EmptyFunctionBlock")
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
|
||||
}
|
||||
|
||||
@Suppress("EmptyFunctionBlock")
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,254 +0,0 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* @author Chris Narkiewicz
|
||||
*
|
||||
* Copyright (C) 2018 Tobias Kaminsky
|
||||
* Copyright (C) 2018 Nextcloud GmbH.
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.ui.activity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
import android.webkit.JavascriptInterface;
|
||||
|
||||
import com.nextcloud.client.account.CurrentAccountProvider;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.network.ClientFactory;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.datamodel.OCFile;
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount;
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
import com.owncloud.android.operations.RichDocumentsCreateAssetOperation;
|
||||
import com.owncloud.android.ui.asynctasks.PrintAsyncTask;
|
||||
import com.owncloud.android.ui.asynctasks.RichDocumentsLoadUrlTask;
|
||||
import com.owncloud.android.ui.fragment.OCFileListFragment;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.FileStorageUtils;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
|
||||
/**
|
||||
* Opens document for editing via Richdocuments app in a web view
|
||||
*/
|
||||
public class RichDocumentsEditorWebView extends EditorWebView {
|
||||
private static final int REQUEST_REMOTE_FILE = 100;
|
||||
private static final String URL = "URL";
|
||||
private static final String HYPERLINK = "Url";
|
||||
private static final String TYPE = "Type";
|
||||
private static final String PRINT = "print";
|
||||
private static final String SLIDESHOW = "slideshow";
|
||||
private static final String NEW_NAME = "NewName";
|
||||
|
||||
@Inject
|
||||
protected CurrentAccountProvider currentAccountProvider;
|
||||
|
||||
@Inject
|
||||
protected ClientFactory clientFactory;
|
||||
|
||||
@SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT_INTERFACE")
|
||||
@Override
|
||||
protected void postOnCreate() {
|
||||
super.postOnCreate();
|
||||
|
||||
getWebView().addJavascriptInterface(new RichDocumentsMobileInterface(), "RichDocumentsMobileInterface");
|
||||
|
||||
// load url in background
|
||||
loadUrl(getIntent().getStringExtra(EXTRA_URL));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
}
|
||||
|
||||
private void openFileChooser() {
|
||||
Intent action = new Intent(this, FilePickerActivity.class);
|
||||
action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/");
|
||||
startActivityForResult(action, REQUEST_REMOTE_FILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_REMOTE_FILE:
|
||||
handleRemoteFile(data);
|
||||
break;
|
||||
|
||||
default:
|
||||
// unexpected, do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
super.handleActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private void handleRemoteFile(Intent data) {
|
||||
OCFile file = data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES);
|
||||
|
||||
new Thread(() -> {
|
||||
User user = currentAccountProvider.getUser();
|
||||
RichDocumentsCreateAssetOperation operation = new RichDocumentsCreateAssetOperation(file.getRemotePath());
|
||||
RemoteOperationResult result = operation.execute(user, this);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
String asset = (String) result.getSingleData();
|
||||
|
||||
runOnUiThread(() -> getWebView().evaluateJavascript("OCA.RichDocuments.documentsMain.postAsset('" +
|
||||
file.getFileName() + "', '" + asset + "');", null));
|
||||
} else {
|
||||
runOnUiThread(() -> DisplayUtils.showSnackMessage(this, "Inserting image failed!"));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putString(EXTRA_URL, url);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
url = savedInstanceState.getString(EXTRA_URL);
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
getWebView().evaluateJavascript("if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " +
|
||||
"{ OCA.RichDocuments.documentsMain.postGrabFocus(); }",
|
||||
null);
|
||||
}
|
||||
|
||||
private void printFile(Uri url) {
|
||||
OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount();
|
||||
|
||||
if (account == null) {
|
||||
DisplayUtils.showSnackMessage(getWebView(), getString(R.string.failed_to_print));
|
||||
return;
|
||||
}
|
||||
|
||||
File targetFile = new File(FileStorageUtils.getTemporalPath(account.getName()) + "/print.pdf");
|
||||
|
||||
new PrintAsyncTask(targetFile, url.toString(), new WeakReference<>(this)).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadUrl(String url) {
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
new RichDocumentsLoadUrlTask(this, getUser().get(), getFile()).execute();
|
||||
} else {
|
||||
super.loadUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
private void showSlideShow(Uri url) {
|
||||
Intent intent = new Intent(this, ExternalSiteWebView.class);
|
||||
intent.putExtra(ExternalSiteWebView.EXTRA_URL, url.toString());
|
||||
intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false);
|
||||
intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_TOOLBAR, false);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private class RichDocumentsMobileInterface extends MobileInterface {
|
||||
@JavascriptInterface
|
||||
public void insertGraphic() {
|
||||
openFileChooser();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void documentLoaded() {
|
||||
runOnUiThread(RichDocumentsEditorWebView.this::hideLoading);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void downloadAs(String json) {
|
||||
try {
|
||||
JSONObject downloadJson = new JSONObject(json);
|
||||
|
||||
Uri url = Uri.parse(downloadJson.getString(URL));
|
||||
|
||||
switch (downloadJson.getString(TYPE)) {
|
||||
case PRINT:
|
||||
printFile(url);
|
||||
break;
|
||||
|
||||
case SLIDESHOW:
|
||||
showSlideShow(url);
|
||||
break;
|
||||
|
||||
default:
|
||||
downloadFile(url);
|
||||
break;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log_OC.e(this, "Failed to parse download json message: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void fileRename(String renameString) {
|
||||
// when shared file is renamed in another instance, we will get notified about it
|
||||
// need to change filename for sharing
|
||||
try {
|
||||
JSONObject renameJson = new JSONObject(renameString);
|
||||
String newName = renameJson.getString(NEW_NAME);
|
||||
getFile().setFileName(newName);
|
||||
} catch (JSONException e) {
|
||||
Log_OC.e(this, "Failed to parse rename json message: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void paste() {
|
||||
// Javascript cannot do this by itself, so help out.
|
||||
getWebView().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE));
|
||||
getWebView().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE));
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void hyperlink(String hyperlink) {
|
||||
try {
|
||||
String url = new JSONObject(hyperlink).getString(HYPERLINK);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(url));
|
||||
startActivity(intent);
|
||||
} catch (JSONException e) {
|
||||
Log_OC.e(this, "Failed to parse download json message: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* @author Chris Narkiewicz
|
||||
*
|
||||
* Copyright (C) 2018 Tobias Kaminsky
|
||||
* Copyright (C) 2018 Nextcloud GmbH.
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.owncloud.android.ui.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.KeyEvent
|
||||
import android.webkit.JavascriptInterface
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.nextcloud.client.account.CurrentAccountProvider
|
||||
import com.nextcloud.client.network.ClientFactory
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.RichDocumentsCreateAssetOperation
|
||||
import com.owncloud.android.ui.asynctasks.PrintAsyncTask
|
||||
import com.owncloud.android.ui.asynctasks.RichDocumentsLoadUrlTask
|
||||
import com.owncloud.android.ui.fragment.OCFileListFragment
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.FileStorageUtils
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Opens document for editing via Richdocuments app in a web view
|
||||
*/
|
||||
class RichDocumentsEditorWebView : EditorWebView() {
|
||||
@JvmField
|
||||
@Inject
|
||||
var currentAccountProvider: CurrentAccountProvider? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var clientFactory: ClientFactory? = null
|
||||
|
||||
private var activityResult: ActivityResultLauncher<Intent>? = null
|
||||
|
||||
@SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT_INTERFACE")
|
||||
override fun postOnCreate() {
|
||||
super.postOnCreate()
|
||||
|
||||
webView.addJavascriptInterface(RichDocumentsMobileInterface(), "RichDocumentsMobileInterface")
|
||||
|
||||
intent.getStringExtra(EXTRA_URL)?.let {
|
||||
loadUrl(it)
|
||||
}
|
||||
|
||||
registerActivityResult()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
}
|
||||
|
||||
private fun openFileChooser() {
|
||||
val action = Intent(this, FilePickerActivity::class.java)
|
||||
action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/")
|
||||
activityResult?.launch(action)
|
||||
}
|
||||
|
||||
private fun registerActivityResult() {
|
||||
activityResult =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (RESULT_OK == result.resultCode) {
|
||||
result.data?.let {
|
||||
handleRemoteFile(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRemoteFile(data: Intent) {
|
||||
val file = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES, OCFile::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES)
|
||||
}
|
||||
|
||||
Thread {
|
||||
val user = currentAccountProvider?.user
|
||||
val operation = RichDocumentsCreateAssetOperation(file?.remotePath)
|
||||
val result = operation.execute(user, this)
|
||||
if (result.isSuccess) {
|
||||
val asset = result.singleData as String
|
||||
runOnUiThread {
|
||||
webView.evaluateJavascript(
|
||||
"OCA.RichDocuments.documentsMain.postAsset('" +
|
||||
file?.fileName + "', '" + asset + "');",
|
||||
null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
runOnUiThread { DisplayUtils.showSnackMessage(this, "Inserting image failed!") }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putString(EXTRA_URL, url)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
url = savedInstanceState.getString(EXTRA_URL)
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
webView.evaluateJavascript(
|
||||
"if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " +
|
||||
"{ OCA.RichDocuments.documentsMain.postGrabFocus(); }",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun printFile(url: Uri) {
|
||||
val account = accountManager.currentOwnCloudAccount
|
||||
if (account == null) {
|
||||
DisplayUtils.showSnackMessage(webView, getString(R.string.failed_to_print))
|
||||
return
|
||||
}
|
||||
val targetFile = File(FileStorageUtils.getTemporalPath(account.name) + "/print.pdf")
|
||||
PrintAsyncTask(targetFile, url.toString(), WeakReference(this)).execute()
|
||||
}
|
||||
|
||||
public override fun loadUrl(url: String) {
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
RichDocumentsLoadUrlTask(this, user.get(), file).execute()
|
||||
} else {
|
||||
super.loadUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSlideShow(url: Uri) {
|
||||
val intent = Intent(this, ExternalSiteWebView::class.java)
|
||||
intent.putExtra(EXTRA_URL, url.toString())
|
||||
intent.putExtra(EXTRA_SHOW_SIDEBAR, false)
|
||||
intent.putExtra(EXTRA_SHOW_TOOLBAR, false)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private inner class RichDocumentsMobileInterface : MobileInterface() {
|
||||
@JavascriptInterface
|
||||
fun insertGraphic() {
|
||||
openFileChooser()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun documentLoaded() {
|
||||
runOnUiThread { hideLoading() }
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun downloadAs(json: String?) {
|
||||
try {
|
||||
json ?: return
|
||||
val downloadJson = JSONObject(json)
|
||||
val url = Uri.parse(downloadJson.getString(URL))
|
||||
when (downloadJson.getString(TYPE)) {
|
||||
PRINT -> printFile(url)
|
||||
SLIDESHOW -> showSlideShow(url)
|
||||
else -> downloadFile(url)
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
Log_OC.e(this, "Failed to parse download json message: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun fileRename(renameString: String?) {
|
||||
// when shared file is renamed in another instance, we will get notified about it
|
||||
// need to change filename for sharing
|
||||
try {
|
||||
renameString ?: return
|
||||
val renameJson = JSONObject(renameString)
|
||||
val newName = renameJson.getString(NEW_NAME)
|
||||
file.fileName = newName
|
||||
} catch (e: JSONException) {
|
||||
Log_OC.e(this, "Failed to parse rename json message: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun paste() {
|
||||
// Javascript cannot do this by itself, so help out.
|
||||
webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE))
|
||||
webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE))
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun hyperlink(hyperlink: String?) {
|
||||
try {
|
||||
hyperlink ?: return
|
||||
val url = JSONObject(hyperlink).getString(HYPERLINK)
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(url)
|
||||
startActivity(intent)
|
||||
} catch (e: JSONException) {
|
||||
Log_OC.e(this, "Failed to parse download json message: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val URL = "URL"
|
||||
private const val HYPERLINK = "Url"
|
||||
private const val TYPE = "Type"
|
||||
private const val PRINT = "print"
|
||||
private const val SLIDESHOW = "slideshow"
|
||||
private const val NEW_NAME = "NewName"
|
||||
}
|
||||
}
|
|
@ -61,6 +61,7 @@ import java.util.List;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
@ -258,6 +259,8 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
|
|||
|
||||
checkWritableFolder(mCurrentDir);
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
|
||||
|
||||
Log_OC.d(TAG, "onCreate() end");
|
||||
}
|
||||
|
||||
|
@ -369,43 +372,45 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (isSearchOpen() && mSearchView != null) {
|
||||
mSearchView.setQuery("", false);
|
||||
mFileListFragment.onClose();
|
||||
mSearchView.onActionViewCollapsed();
|
||||
setDrawerIndicatorEnabled(isDrawerIndicatorAvailable());
|
||||
} else {
|
||||
if (mDirectories.getCount() <= SINGLE_DIR) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (isSearchOpen() && mSearchView != null) {
|
||||
mSearchView.setQuery("", false);
|
||||
mFileListFragment.onClose();
|
||||
mSearchView.onActionViewCollapsed();
|
||||
setDrawerIndicatorEnabled(isDrawerIndicatorAvailable());
|
||||
} else {
|
||||
if (mDirectories.getCount() <= SINGLE_DIR) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
File parentFolder = mCurrentDir.getParentFile();
|
||||
if (!parentFolder.canRead()) {
|
||||
checkLocalStoragePathPickerPermission();
|
||||
return;
|
||||
}
|
||||
File parentFolder = mCurrentDir.getParentFile();
|
||||
if (!parentFolder.canRead()) {
|
||||
checkLocalStoragePathPickerPermission();
|
||||
return;
|
||||
}
|
||||
|
||||
popDirname();
|
||||
mFileListFragment.onNavigateUp();
|
||||
mCurrentDir = mFileListFragment.getCurrentDirectory();
|
||||
checkWritableFolder(mCurrentDir);
|
||||
popDirname();
|
||||
mFileListFragment.onNavigateUp();
|
||||
mCurrentDir = mFileListFragment.getCurrentDirectory();
|
||||
checkWritableFolder(mCurrentDir);
|
||||
|
||||
if (mCurrentDir.getParentFile() == null) {
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
if (mCurrentDir.getParentFile() == null) {
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
// invalidate checked state when navigating directories
|
||||
if (!mLocalFolderPickerMode) {
|
||||
setSelectAllMenuItem(mOptionsMenu.findItem(R.id.action_select_all), false);
|
||||
}
|
||||
}
|
||||
|
||||
// invalidate checked state when navigating directories
|
||||
if (!mLocalFolderPickerMode) {
|
||||
setSelectAllMenuItem(mOptionsMenu.findItem(R.id.action_select_all), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
|
|
|
@ -41,6 +41,7 @@ import com.owncloud.android.utils.DisplayUtils
|
|||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class ReceiveExternalFilesAdapter(
|
||||
private val files: List<OCFile>,
|
||||
private val context: Context,
|
||||
|
@ -132,6 +133,7 @@ class ReceiveExternalFilesAdapter(
|
|||
thumbnailImageView.setImageDrawable(icon)
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
private fun setupThumbnailForImage(thumbnailImageView: ImageView, file: OCFile) {
|
||||
var thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(file.remoteId.toString())
|
||||
if (thumbnail != null && !file.isUpdateThumbnailNeeded) {
|
||||
|
|
|
@ -62,6 +62,8 @@ import com.owncloud.android.lib.common.utils.Log_OC;
|
|||
import com.owncloud.android.operations.RefreshFolderOperation;
|
||||
import com.owncloud.android.ui.activity.ConflictsResolveActivity;
|
||||
import com.owncloud.android.ui.activity.FileActivity;
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity;
|
||||
import com.owncloud.android.ui.preview.PreviewImageFragment;
|
||||
import com.owncloud.android.utils.DisplayUtils;
|
||||
import com.owncloud.android.utils.MimeTypeUtil;
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils;
|
||||
|
@ -346,12 +348,15 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
|
|||
itemViewHolder.binding.uploadRightButton.setOnClickListener(v -> removeUpload(item));
|
||||
}
|
||||
itemViewHolder.binding.uploadRightButton.setVisibility(View.VISIBLE);
|
||||
} else { // UploadStatus.UPLOAD_SUCCESS
|
||||
} else { // UploadStatus.UPLOAD_SUCCEEDED
|
||||
itemViewHolder.binding.uploadRightButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
itemViewHolder.binding.uploadListItemLayout.setOnClickListener(null);
|
||||
|
||||
// Set icon or thumbnail
|
||||
itemViewHolder.binding.thumbnail.setImageResource(R.drawable.file);
|
||||
|
||||
// click on item
|
||||
if (item.getUploadStatus() == UploadStatus.UPLOAD_FAILED) {
|
||||
final UploadResult uploadResult = item.getLastResult();
|
||||
|
@ -381,12 +386,15 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
|
|||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
itemViewHolder.binding.uploadListItemLayout.setOnClickListener(v -> onUploadItemClick(item));
|
||||
} else if (item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED){
|
||||
itemViewHolder.binding.uploadListItemLayout.setOnClickListener(v -> onUploadedItemClick(item));
|
||||
}
|
||||
|
||||
// Set icon or thumbnail
|
||||
itemViewHolder.binding.thumbnail.setImageResource(R.drawable.file);
|
||||
|
||||
// click on thumbnail to open locally
|
||||
if (item.getUploadStatus() != UploadStatus.UPLOAD_SUCCEEDED){
|
||||
itemViewHolder.binding.thumbnail.setOnClickListener(v -> onUploadingItemClick(item));
|
||||
}
|
||||
|
||||
/*
|
||||
* Cancellation needs do be checked and done before changing the drawable in fileIcon, or
|
||||
|
@ -738,7 +746,10 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void onUploadItemClick(OCUpload file) {
|
||||
/**
|
||||
* Open local file.
|
||||
*/
|
||||
private void onUploadingItemClick(OCUpload file) {
|
||||
File f = new File(file.getLocalPath());
|
||||
if (!f.exists()) {
|
||||
DisplayUtils.showSnackMessage(parentActivity, R.string.local_file_not_found_message);
|
||||
|
@ -747,6 +758,30 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open remote file.
|
||||
*/
|
||||
private void onUploadedItemClick(OCUpload upload) {
|
||||
final OCFile file = parentActivity.getStorageManager().getFileByEncryptedRemotePath(upload.getRemotePath());
|
||||
if (file == null){
|
||||
DisplayUtils.showSnackMessage(parentActivity, R.string.error_retrieving_file);
|
||||
Log_OC.i(TAG, "Could not find uploaded file on remote.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (PreviewImageFragment.canBePreviewed(file)){
|
||||
//show image preview and stay in uploads tab
|
||||
Intent intent = FileDisplayActivity.openFileIntent(parentActivity, parentActivity.getUser().get(), file);
|
||||
parentActivity.startActivity(intent);
|
||||
}else{
|
||||
Intent intent = new Intent(parentActivity, FileDisplayActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(FileDisplayActivity.KEY_FILE_PATH, upload.getRemotePath());
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
parentActivity.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Open file with app associates with its MIME type. If MIME type unknown, show list with all apps.
|
||||
|
|
|
@ -17,36 +17,36 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/card_view"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/standard_double_margin"
|
||||
card_view:cardCornerRadius="4dp"
|
||||
card_view:strokeWidth="0dp"
|
||||
card_view:cardCornerRadius="16dp"
|
||||
card_view:cardElevation="@dimen/dialog_elevation">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/card_view_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/standard_padding"
|
||||
android:paddingTop="@dimen/standard_padding"
|
||||
android:paddingEnd="@dimen/standard_padding"
|
||||
android:paddingBottom="@dimen/standard_half_padding">
|
||||
android:padding="@dimen/standard_padding">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -56,7 +56,7 @@
|
|||
android:textSize="@dimen/two_line_primary_text_size"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/explanation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -96,12 +96,14 @@
|
|||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Button.Borderless"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/common_cancel" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</ScrollView>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<string name="auth_host_url">Adresa serveru https://…</string>
|
||||
<string name="auth_incorrect_address_title">Nesprávný formát adresy serveru</string>
|
||||
<string name="auth_incorrect_path_title">Server nenalezen</string>
|
||||
<string name="auth_no_net_conn_title">Žádné síťové spojení</string>
|
||||
<string name="auth_no_net_conn_title">Nepřipojeno k síti</string>
|
||||
<string name="auth_nossl_plain_ok_title">Zabezpečené spojení není k dispozici.</string>
|
||||
<string name="auth_not_configured_title">Nesprávně formulované nastavení pro server</string>
|
||||
<string name="auth_oauth_error">Neúspěšné ověření se</string>
|
||||
|
@ -163,11 +163,11 @@
|
|||
<string name="contactlist_item_icon">Ikona uživatele v seznamu kontaktů</string>
|
||||
<string name="contactlist_no_permission">Nejsou udělena oprávnění, proto nebylo nic naimportováno.</string>
|
||||
<string name="contacts">Kontakty</string>
|
||||
<string name="contacts_backup_button">Zálohovat nyní</string>
|
||||
<string name="contacts_backup_button">Zazálohovat nyní</string>
|
||||
<string name="contacts_preferences_backup_scheduled">Zálohování naplánováno a brzy začne</string>
|
||||
<string name="contacts_preferences_import_scheduled">Import naplánován a brzy začne</string>
|
||||
<string name="contacts_preferences_no_file_found">Nenalezen žádný soubor</string>
|
||||
<string name="contacts_preferences_something_strange_happened">Nepodařilo se najít vaši poslední zálohu!</string>
|
||||
<string name="contacts_preferences_something_strange_happened">Nepodařilo se najít vaši nejaktuálnější zálohu!</string>
|
||||
<string name="copied_to_clipboard">Zkopírováno do schránky</string>
|
||||
<string name="copy_file_error">Při pokusu o zkopírování tohoto souboru či složky došlo k chybě</string>
|
||||
<string name="copy_file_invalid_into_descendent">Není možné zkopírovat složku do některé z jejích vlastních podsložek</string>
|
||||
|
@ -363,7 +363,7 @@
|
|||
<string name="file_migration_migrating">Přesouvání dat…</string>
|
||||
<string name="file_migration_ok_finished">Dokončeno</string>
|
||||
<string name="file_migration_override_data_folder">Nahradit</string>
|
||||
<string name="file_migration_preparing">Příprava migrace…</string>
|
||||
<string name="file_migration_preparing">Příprava stěhování…</string>
|
||||
<string name="file_migration_restoring_accounts_configuration">Obnovování nastavení účtu…</string>
|
||||
<string name="file_migration_saving_accounts_configuration">Ukládání nastavení účtu…</string>
|
||||
<string name="file_migration_source_not_readable">Opravdu chcete změnit složku pro ukládání dat na %1$s?\n\nPoznámka: Všechna data bude třeba znovu stáhnout.</string>
|
||||
|
@ -598,7 +598,7 @@
|
|||
<string name="prefs_enable_media_scan_notifications_summary">Upozorňovat na nově nalezené složky s médii</string>
|
||||
<string name="prefs_gpl_v2">GNU General Public License, verze 2</string>
|
||||
<string name="prefs_help">Nápověda</string>
|
||||
<string name="prefs_imprint">Imprint</string>
|
||||
<string name="prefs_imprint">Impresum</string>
|
||||
<string name="prefs_instant_behaviour_dialogTitle">Původní soubor bude…</string>
|
||||
<string name="prefs_instant_behaviour_title">Původní soubor bude…</string>
|
||||
<string name="prefs_instant_upload_path_use_date_subfolders_summary">Ukládat v podsložkách podle data</string>
|
||||
|
@ -935,7 +935,7 @@
|
|||
<string name="user_info_address">Adresa</string>
|
||||
<string name="user_info_email">E-mail</string>
|
||||
<string name="user_info_phone">Telefonní číslo</string>
|
||||
<string name="user_info_twitter">Twitter</string>
|
||||
<string name="user_info_twitter">X (Twitter)</string>
|
||||
<string name="user_info_website">Webové stránky</string>
|
||||
<string name="user_information_retrieval_error">Chyba při načítání informací o uživateli</string>
|
||||
<string name="userinfo_no_info_headline">Nezadány žádné osobní údaje</string>
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<string name="actionbar_copy">Kopiuj</string>
|
||||
<string name="actionbar_mkdir">Nowy katalog</string>
|
||||
<string name="actionbar_move">Przenieś</string>
|
||||
<string name="actionbar_move_or_copy">Przenieś lub kopiuj</string>
|
||||
<string name="actionbar_open_with">Otwórz za pomocą</string>
|
||||
<string name="actionbar_search">Wyszukaj</string>
|
||||
<string name="actionbar_see_details">Szczegóły</string>
|
||||
|
@ -324,6 +325,7 @@
|
|||
<string name="file_delete">Usuń</string>
|
||||
<string name="file_detail_activity_error">Błąd podczas pobierania aktywności dla pliku</string>
|
||||
<string name="file_details_no_content">Nie udało się załadować szczegółów</string>
|
||||
<string name="file_downloader_notification_title_prefix">Pobieranie \u0020</string>
|
||||
<string name="file_icon">Plik</string>
|
||||
<string name="file_keep">Zachowaj</string>
|
||||
<string name="file_list_empty">Wyślij lub zsynchronizuj pliki z urządzeniami.</string>
|
||||
|
@ -944,7 +946,9 @@
|
|||
<string name="wait_a_moment">Proszę czekać…</string>
|
||||
<string name="wait_checking_credentials">Sprawdzanie danych</string>
|
||||
<string name="wait_for_tmp_copy_from_private_storage">Kopiowanie pliku z prywatnego magazynu</string>
|
||||
<string name="webview_version_check_alert_dialog_message">Aby się zalogować, zaktualizuj aplikację WebView systemu Android</string>
|
||||
<string name="webview_version_check_alert_dialog_positive_button_title">Aktualizuj</string>
|
||||
<string name="webview_version_check_alert_dialog_title">Zaktualizuj WebView systemu Android</string>
|
||||
<string name="what_s_new_image">Jaki jest nowy obraz</string>
|
||||
<string name="whats_new_skip">Pomiń</string>
|
||||
<string name="whats_new_title">Co nowego w %1$s</string>
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
<item name="colorPrimary">@color/text_color</item>
|
||||
<item name="colorOnSurface">@color/text_color</item>
|
||||
<item name="colorError">@color/hwSecurityRed</item>
|
||||
<item name="editTextStyle">@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox</item>
|
||||
<item name="editTextStyle">@style/Widget.Material3.TextInputEditText.OutlinedBox</item>
|
||||
</style>
|
||||
|
||||
<style name="TextInputLayoutInputWarning" parent="Widget.App.TextInputLayout">
|
||||
|
@ -102,7 +102,7 @@
|
|||
<item name="android:buttonBarButtonStyle">@style/FallbackTheming.Dialog.ButtonStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="FallbackTheming.Dialog.ButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
|
||||
<style name="FallbackTheming.Dialog.ButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
|
||||
<item name="android:textColor">@color/text_color</item>
|
||||
</style>
|
||||
|
||||
|
@ -363,7 +363,7 @@
|
|||
|
||||
<style name="Widget.App.Login.TextInputLayout" parent="Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
<item name="materialThemeOverlay">@style/ThemeOverlay.App.Login.TextInputLayout</item>
|
||||
<item name="shapeAppearance">@style/ShapeAppearance.MaterialComponents.SmallComponent</item>
|
||||
<item name="shapeAppearance">@style/ShapeAppearance.Material3.SmallComponent</item>
|
||||
<item name="hintTextColor">?attr/colorOnSurface</item>
|
||||
</style>
|
||||
|
||||
|
@ -371,9 +371,9 @@
|
|||
<item name="colorPrimary">@color/white</item>
|
||||
<item name="colorOnSurface">@color/white</item>
|
||||
<item name="colorError">@color/hwSecurityRed</item>
|
||||
<item name="textAppearanceSubtitle1">@style/TextAppearance.MaterialComponents.Subtitle1</item>
|
||||
<item name="textAppearanceCaption">@style/TextAppearance.MaterialComponents.Caption</item>
|
||||
<item name="editTextStyle">@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox</item>
|
||||
<item name="textAppearanceSubtitle1">@style/TextAppearance.Material3.BodyLarge</item>
|
||||
<item name="textAppearanceCaption">@style/TextAppearance.Material3.BodySmall</item>
|
||||
<item name="editTextStyle">@style/Widget.Material3.TextInputEditText.OutlinedBox</item>
|
||||
</style>
|
||||
|
||||
<style name="TextInputLayoutLogin" parent="Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
@ -476,11 +476,11 @@
|
|||
</style>
|
||||
|
||||
|
||||
<style name="ThemeOverlay.App.BottomSheetDialog" parent="@style/ThemeOverlay.MaterialComponents.BottomSheetDialog">
|
||||
<style name="ThemeOverlay.App.BottomSheetDialog" parent="@style/ThemeOverlay.Material3.BottomSheetDialog">
|
||||
<item name="bottomSheetStyle">@style/App.BottomSheetDialog</item>
|
||||
</style>
|
||||
|
||||
<style name="App.BottomSheetDialog" parent="Widget.MaterialComponents.BottomSheet.Modal">
|
||||
<style name="App.BottomSheetDialog" parent="Widget.Material3.BottomSheet.Modal">
|
||||
<item name="backgroundTint">@color/bg_default</item>
|
||||
</style>
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ class LocalConnectionTest {
|
|||
|
||||
// THEN
|
||||
// no binding is performed
|
||||
verify(exactly = 0) { context.bindService(any(), any(), any()) }
|
||||
verify(exactly = 0) { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -76,12 +76,12 @@ class LocalConnectionTest {
|
|||
|
||||
// WHEN
|
||||
// bind requested
|
||||
every { context.bindService(mockIntent, any(), any()) } returns true
|
||||
every { context.bindService(mockIntent!!, any(), Context.BIND_AUTO_CREATE) } returns true
|
||||
connection.bind()
|
||||
|
||||
// THEN
|
||||
// service bound
|
||||
verify { context.bindService(mockIntent, any(), any()) }
|
||||
verify { context.bindService(mockIntent!!, any(), Context.BIND_AUTO_CREATE) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue