Implement backup and restore function

This commit is contained in:
M M Arif 2024-03-24 16:06:53 +05:00
parent 9b0d76bf38
commit 5cee335928
16 changed files with 1040 additions and 175 deletions

View File

@ -99,6 +99,7 @@ Thanks to all the open source libraries, contributors, and donors.
- [lucide-icons/lucide](https://github.com/lucide-icons/lucide)
- [primer/octicons](https://github.com/primer/octicons)
- [google/material-design-icons](https://github.com/google/material-design-icons)
- [tabler/tabler-icons](https://github.com/tabler/tabler-icons)
[Follow me on Fediverse - mastodon.social/@mmarif](https://mastodon.social/@mmarif)

View File

@ -177,6 +177,9 @@
android:name=".activities.CreateNoteActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".activities.SettingsBackupRestoreActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation" />
<meta-data
android:name="com.samsung.android.keepalive.density"

View File

@ -107,14 +107,14 @@ public class AddNewAccountActivity extends BaseActivity {
return;
}
if (instanceUrlET.equals("")) {
if (instanceUrlET.isEmpty()) {
SnackBar.error(
ctx, findViewById(android.R.id.content), getString(R.string.emptyFieldURL));
return;
}
if (loginToken.equals("")) {
if (loginToken.isEmpty()) {
SnackBar.error(
ctx,

View File

@ -217,7 +217,7 @@ public class CreateIssueActivity extends BaseActivity
private void checkForAttachments() {
if (contentUri.size() > 0) {
if (!contentUri.isEmpty()) {
BottomSheetAttachmentsBinding bottomSheetAttachmentsBinding =
BottomSheetAttachmentsBinding.inflate(getLayoutInflater());
@ -409,7 +409,7 @@ public class CreateIssueActivity extends BaseActivity
String newIssueDueDateForm =
Objects.requireNonNull(viewBinding.newIssueDueDate.getText()).toString();
if (newIssueTitleForm.equals("")) {
if (newIssueTitleForm.isEmpty()) {
SnackBar.error(
ctx, findViewById(android.R.id.content), getString(R.string.issueTitleEmpty));
@ -479,7 +479,7 @@ public class CreateIssueActivity extends BaseActivity
assert response2.body() != null;
if (contentUri.size() > 0) {
if (!contentUri.isEmpty()) {
processAttachments(response2.body().getNumber());
contentUri.clear();
} else {
@ -536,7 +536,7 @@ public class CreateIssueActivity extends BaseActivity
milestonesList.put(ms.getTitle(), ms);
assert milestonesList_ != null;
if (milestonesList_.size() > 0) {
if (!milestonesList_.isEmpty()) {
for (Milestone milestone : milestonesList_) {

View File

@ -1,19 +1,30 @@
package org.mian.gitnex.activities;
import static org.mian.gitnex.helpers.BackupUtil.checkpointIfWALEnabled;
import static org.mian.gitnex.helpers.BackupUtil.copyFile;
import static org.mian.gitnex.helpers.BackupUtil.getTempDir;
import static org.mian.gitnex.helpers.BackupUtil.unzip;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import io.mikael.urlbuilder.UrlBuilder;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import okhttp3.Credentials;
import org.gitnex.tea4j.v2.models.AccessToken;
@ -42,48 +53,36 @@ import retrofit2.Callback;
*/
public class LoginActivity extends BaseActivity {
private Button loginButton;
private EditText instanceUrlET, loginUidET, loginPassword, otpCode, loginTokenCode;
private AutoCompleteTextView protocolSpinner;
private RadioGroup loginMethod;
private ActivityLoginBinding activityLoginBinding;
private String device_id = "token";
private String selectedProtocol;
private URI instanceUrl;
private Version giteaVersion;
private int maxResponseItems = 50;
private int defaultPagingNumber = 25;
private final String DATABASE_NAME = "gitnex";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityLoginBinding activityLoginBinding =
ActivityLoginBinding.inflate(getLayoutInflater());
activityLoginBinding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(activityLoginBinding.getRoot());
NetworkStatusObserver networkStatusObserver = NetworkStatusObserver.getInstance(ctx);
loginButton = activityLoginBinding.loginButton;
instanceUrlET = activityLoginBinding.instanceUrl;
loginUidET = activityLoginBinding.loginUid;
loginPassword = activityLoginBinding.loginPasswd;
otpCode = activityLoginBinding.otpCode;
protocolSpinner = activityLoginBinding.httpsSpinner;
loginMethod = activityLoginBinding.loginMethod;
loginTokenCode = activityLoginBinding.loginTokenCode;
activityLoginBinding.appVersion.setText(AppUtil.getAppVersion(appCtx));
ArrayAdapter<Protocol> adapterProtocols =
new ArrayAdapter<>(
LoginActivity.this, R.layout.list_spinner_items, Protocol.values());
instanceUrlET.setText(getIntent().getStringExtra("instanceUrl"));
activityLoginBinding.instanceUrl.setText(getIntent().getStringExtra("instanceUrl"));
protocolSpinner.setAdapter(adapterProtocols);
protocolSpinner.setSelection(0);
protocolSpinner.setOnItemClickListener(
activityLoginBinding.httpsSpinner.setAdapter(adapterProtocols);
activityLoginBinding.httpsSpinner.setSelection(0);
activityLoginBinding.httpsSpinner.setOnItemClickListener(
(parent, view, position, id) -> {
selectedProtocol = String.valueOf(parent.getItemAtPosition(position));
@ -95,7 +94,7 @@ public class LoginActivity extends BaseActivity {
}
});
if (R.id.loginToken == loginMethod.getCheckedRadioButtonId()) {
if (R.id.loginToken == activityLoginBinding.loginMethod.getCheckedRadioButtonId()) {
AppUtil.setMultiVisibility(
View.GONE,
findViewById(R.id.login_uidLayout),
@ -111,7 +110,7 @@ public class LoginActivity extends BaseActivity {
findViewById(R.id.loginTokenCodeLayout).setVisibility(View.GONE);
}
loginMethod.setOnCheckedChangeListener(
activityLoginBinding.loginMethod.setOnCheckedChangeListener(
(group, checkedId) -> {
if (checkedId == R.id.loginToken) {
AppUtil.setMultiVisibility(
@ -138,7 +137,7 @@ public class LoginActivity extends BaseActivity {
enableProcessButton();
} else {
disableProcessButton();
loginButton.setText(
activityLoginBinding.loginButton.setText(
getResources().getString(R.string.btnLogin));
SnackBar.error(
ctx,
@ -149,11 +148,29 @@ public class LoginActivity extends BaseActivity {
loadDefaults();
loginButton.setOnClickListener(
activityLoginBinding.loginButton.setOnClickListener(
view -> {
disableProcessButton();
login();
});
activityLoginBinding.restoreFromBackup.setOnClickListener(
restoreDb -> {
MaterialAlertDialogBuilder materialAlertDialogBuilder =
new MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.restore)
.setMessage(
getResources()
.getString(R.string.restoreFromBackupPopupText))
.setNeutralButton(
R.string.cancelButton,
(dialog, which) -> dialog.dismiss())
.setPositiveButton(
R.string.restore,
(dialog, which) -> requestRestoreFile());
materialAlertDialogBuilder.create().show();
});
}
private void login() {
@ -170,21 +187,33 @@ public class LoginActivity extends BaseActivity {
return;
}
String loginUid = loginUidET.getText().toString().replaceAll("[\\uFEFF]", "").trim();
String loginPass = loginPassword.getText().toString().trim();
String loginUid =
Objects.requireNonNull(activityLoginBinding.loginUid.getText())
.toString()
.replaceAll("[\\uFEFF]", "")
.trim();
String loginPass =
Objects.requireNonNull(activityLoginBinding.loginPasswd.getText())
.toString()
.trim();
String loginToken =
loginTokenCode.getText().toString().replaceAll("[\\uFEFF|#]", "").trim();
Objects.requireNonNull(activityLoginBinding.loginTokenCode.getText())
.toString()
.replaceAll("[\\uFEFF|#]", "")
.trim();
LoginType loginType =
(loginMethod.getCheckedRadioButtonId() == R.id.loginUsernamePassword)
(activityLoginBinding.loginMethod.getCheckedRadioButtonId()
== R.id.loginUsernamePassword)
? LoginType.BASIC
: LoginType.TOKEN;
URI rawInstanceUrl =
UrlBuilder.fromString(
UrlHelper.fixScheme(
instanceUrlET
.getText()
Objects.requireNonNull(
activityLoginBinding.instanceUrl
.getText())
.toString()
.replaceAll("[\\uFEFF|#]", "")
.trim(),
@ -199,9 +228,10 @@ public class LoginActivity extends BaseActivity {
// cache values to make them available the next time the user wants to log in
tinyDB.putString("loginType", loginType.name().toLowerCase());
tinyDB.putString("instanceUrlRaw", instanceUrlET.getText().toString());
tinyDB.putString(
"instanceUrlRaw", activityLoginBinding.instanceUrl.getText().toString());
if (instanceUrlET.getText().toString().equals("")) {
if (activityLoginBinding.instanceUrl.getText().toString().isEmpty()) {
SnackBar.error(
ctx, findViewById(android.R.id.content), getString(R.string.emptyFieldURL));
@ -211,7 +241,8 @@ public class LoginActivity extends BaseActivity {
if (loginType == LoginType.BASIC) {
if (otpCode.length() != 0 && otpCode.length() != 6) {
if (activityLoginBinding.otpCode.length() != 0
&& activityLoginBinding.otpCode.length() != 6) {
SnackBar.error(
ctx,
@ -221,7 +252,7 @@ public class LoginActivity extends BaseActivity {
return;
}
if (loginUid.equals("")) {
if (loginUid.isEmpty()) {
SnackBar.error(
ctx,
findViewById(android.R.id.content),
@ -230,7 +261,7 @@ public class LoginActivity extends BaseActivity {
return;
}
if (loginPass.equals("")) {
if (loginPass.isEmpty()) {
SnackBar.error(
ctx,
findViewById(android.R.id.content),
@ -240,15 +271,19 @@ public class LoginActivity extends BaseActivity {
}
int loginOTP =
(otpCode.length() > 0)
? Integer.parseInt(otpCode.getText().toString().trim())
(activityLoginBinding.otpCode.length() > 0)
? Integer.parseInt(
Objects.requireNonNull(
activityLoginBinding.otpCode.getText())
.toString()
.trim())
: 0;
versionCheck(loginUid, loginPass, loginOTP, loginToken, loginType);
} else {
if (loginToken.equals("")) {
if (loginToken.isEmpty()) {
SnackBar.error(
ctx,
@ -311,7 +346,7 @@ public class LoginActivity extends BaseActivity {
Call<ServerVersion> callVersion;
if (!loginToken.equals("")) {
if (!loginToken.isEmpty()) {
callVersion =
RetrofitClient.getApiInterface(
@ -700,7 +735,7 @@ public class LoginActivity extends BaseActivity {
AccessToken newToken = responseCreate.body();
assert newToken != null;
if (!newToken.getSha1().equals("")) {
if (!newToken.getSha1().isEmpty()) {
Call<User> call =
RetrofitClient.getApiInterface(
@ -836,20 +871,20 @@ public class LoginActivity extends BaseActivity {
if (tinyDB.getString("loginType").equals(LoginType.BASIC.name().toLowerCase())) {
loginMethod.check(R.id.loginUsernamePassword);
activityLoginBinding.loginMethod.check(R.id.loginUsernamePassword);
} else {
loginMethod.check(R.id.loginToken);
activityLoginBinding.loginMethod.check(R.id.loginToken);
}
if (!tinyDB.getString("instanceUrlRaw").equals("")) {
if (!tinyDB.getString("instanceUrlRaw").isEmpty()) {
instanceUrlET.setText(tinyDB.getString("instanceUrlRaw"));
activityLoginBinding.instanceUrl.setText(tinyDB.getString("instanceUrlRaw"));
}
if (getAccount() != null && getAccount().getAccount() != null) {
loginUidET.setText(getAccount().getAccount().getUserName());
activityLoginBinding.loginUid.setText(getAccount().getAccount().getUserName());
}
if (!tinyDB.getString("uniqueAppId").isEmpty()) {
@ -863,18 +898,109 @@ public class LoginActivity extends BaseActivity {
private void disableProcessButton() {
loginButton.setText(R.string.processingText);
loginButton.setEnabled(false);
activityLoginBinding.loginButton.setText(R.string.processingText);
activityLoginBinding.loginButton.setEnabled(false);
}
private void enableProcessButton() {
loginButton.setText(R.string.btnLogin);
loginButton.setEnabled(true);
activityLoginBinding.loginButton.setText(R.string.btnLogin);
activityLoginBinding.loginButton.setEnabled(true);
}
private enum LoginType {
BASIC,
TOKEN
}
private void requestRestoreFile() {
Intent intentRestore = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intentRestore.addCategory(Intent.CATEGORY_OPENABLE);
intentRestore.setType("*/*");
String[] mimeTypes = {"application/octet-stream", "application/x-zip"};
intentRestore.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
activityRestoreFileLauncher.launch(intentRestore);
}
ActivityResultLauncher<Intent> activityRestoreFileLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
assert result.getData() != null;
Uri restoreFileUri = result.getData().getData();
assert restoreFileUri != null;
try {
InputStream inputStream =
getContentResolver().openInputStream(restoreFileUri);
restoreDatabaseThread(inputStream);
} catch (FileNotFoundException e) {
SnackBar.error(
ctx,
findViewById(android.R.id.content),
getString(R.string.restoreError));
}
}
});
private void restoreDatabaseThread(InputStream inputStream) {
Thread restoreDatabaseThread =
new Thread(
() -> {
boolean exceptionOccurred = false;
try {
String tempDir = getTempDir(ctx).getPath();
unzip(inputStream, tempDir);
checkpointIfWALEnabled(ctx, DATABASE_NAME);
restoreDatabaseFile(ctx, tempDir, DATABASE_NAME);
UserAccountsApi userAccountsApi =
BaseApi.getInstance(ctx, UserAccountsApi.class);
assert userAccountsApi != null;
UserAccount account = userAccountsApi.getAccountById(1);
AppUtil.switchToAccount(ctx, account);
} catch (final Exception e) {
exceptionOccurred = true;
SnackBar.error(
ctx,
findViewById(android.R.id.content),
getString(R.string.restoreError));
} finally {
if (!exceptionOccurred) {
runOnUiThread(this::restartApp);
}
}
});
restoreDatabaseThread.setDaemon(false);
restoreDatabaseThread.start();
}
public void restoreDatabaseFile(Context context, String tempDir, String nameOfFileToRestore)
throws IOException {
File currentDbFile = new File(context.getDatabasePath(DATABASE_NAME).getPath());
File newDbFile = new File(tempDir + "/" + nameOfFileToRestore);
if (newDbFile.exists()) {
copyFile(newDbFile, currentDbFile, false);
}
}
public void restartApp() {
Intent i = ctx.getPackageManager().getLaunchIntentForPackage(ctx.getPackageName());
assert i != null;
startActivity(Intent.makeRestartActivityTask(i.getComponent()));
Runtime.getRuntime().exit(0);
}
}

View File

@ -0,0 +1,285 @@
package org.mian.gitnex.activities;
import static org.mian.gitnex.helpers.BackupUtil.backupDatabaseFile;
import static org.mian.gitnex.helpers.BackupUtil.checkpointIfWALEnabled;
import static org.mian.gitnex.helpers.BackupUtil.copyFile;
import static org.mian.gitnex.helpers.BackupUtil.copyFileWithStreams;
import static org.mian.gitnex.helpers.BackupUtil.getTempDir;
import static org.mian.gitnex.helpers.BackupUtil.unzip;
import static org.mian.gitnex.helpers.BackupUtil.zip;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.mian.gitnex.R;
import org.mian.gitnex.database.api.BaseApi;
import org.mian.gitnex.database.api.UserAccountsApi;
import org.mian.gitnex.database.models.UserAccount;
import org.mian.gitnex.databinding.ActivitySettingsBackupRestoreBinding;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.SnackBar;
/**
* @author M M Arif
*/
public class SettingsBackupRestoreActivity extends BaseActivity {
private final String DATABASE_NAME = "gitnex";
private String BACKUP_DATABASE_NAME;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivitySettingsBackupRestoreBinding viewBinding =
ActivitySettingsBackupRestoreBinding.inflate(getLayoutInflater());
setContentView(viewBinding.getRoot());
viewBinding.topAppBar.setNavigationOnClickListener(v -> finish());
viewBinding.topAppBar.setTitle(
getResources()
.getString(
R.string.backupRestore,
getString(R.string.backup),
getString(R.string.restore)));
BACKUP_DATABASE_NAME = ctx.getString(R.string.appName) + "-" + LocalDate.now() + ".backup";
viewBinding.backupDataFrame.setOnClickListener(
v -> {
MaterialAlertDialogBuilder materialAlertDialogBuilder =
new MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.backup)
.setMessage(
getResources().getString(R.string.backupFilePopupText))
.setNeutralButton(
R.string.cancelButton,
(dialog, which) -> dialog.dismiss())
.setPositiveButton(
R.string.backup,
(dialog, which) -> requestBackupFileDownload());
materialAlertDialogBuilder.create().show();
});
viewBinding.restoreDataFrame.setOnClickListener(
restoreDb -> {
MaterialAlertDialogBuilder materialAlertDialogBuilder =
new MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.restore)
.setMessage(
getResources().getString(R.string.restoreFilePopupText))
.setNeutralButton(
R.string.cancelButton,
(dialog, which) -> dialog.dismiss())
.setPositiveButton(
R.string.restore,
(dialog, which) -> requestRestoreFile());
materialAlertDialogBuilder.create().show();
});
}
ActivityResultLauncher<Intent> activityBackupFileLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
assert result.getData() != null;
Uri backupFileUri = result.getData().getData();
backupDatabaseThread(backupFileUri);
}
});
private void requestBackupFileDownload() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_TITLE, BACKUP_DATABASE_NAME);
intent.setType("application/octet-stream");
activityBackupFileLauncher.launch(intent);
}
private void backupDatabaseThread(Uri backupFileUri) {
List<File> filesToZip = new ArrayList<>();
Thread backupDatabaseThread =
new Thread(
() -> {
File tempDir = getTempDir(ctx);
try {
checkpointIfWALEnabled(ctx, DATABASE_NAME);
File databaseBackupFile =
backupDatabaseFile(
getDatabasePath(DATABASE_NAME).getPath(),
tempDir.getPath() + "/" + DATABASE_NAME);
filesToZip.add(databaseBackupFile);
String tempZipFilename = "temp.backup";
boolean zipFileStatus =
zip(filesToZip, tempDir.getPath(), tempZipFilename);
if (zipFileStatus) {
File tempZipFile = new File(tempDir, tempZipFilename);
Uri zipFileUri = Uri.fromFile(tempZipFile);
InputStream inputStream =
getContentResolver().openInputStream(zipFileUri);
OutputStream outputStream =
getContentResolver().openOutputStream(backupFileUri);
boolean copySucceeded =
copyFileWithStreams(inputStream, outputStream);
SnackBar.success(
ctx,
findViewById(android.R.id.content),
getString(R.string.backupFileSuccess));
if (copySucceeded) {
tempZipFile.delete();
} else {
SnackBar.error(
ctx,
findViewById(android.R.id.content),
getString(R.string.backupFileError));
}
} else {
SnackBar.error(
ctx,
findViewById(android.R.id.content),
getString(R.string.backupFileError));
}
} catch (final Exception e) {
SnackBar.error(
ctx,
findViewById(android.R.id.content),
getString(R.string.backupFileError));
} finally {
for (File file : filesToZip) {
if (file != null && file.exists()) {
file.delete();
}
}
}
});
backupDatabaseThread.setDaemon(false);
backupDatabaseThread.start();
}
private void requestRestoreFile() {
Intent intentRestore = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intentRestore.addCategory(Intent.CATEGORY_OPENABLE);
intentRestore.setType("*/*");
String[] mimeTypes = {"application/octet-stream", "application/x-zip"};
intentRestore.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
activityRestoreFileLauncher.launch(intentRestore);
}
ActivityResultLauncher<Intent> activityRestoreFileLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
assert result.getData() != null;
Uri restoreFileUri = result.getData().getData();
assert restoreFileUri != null;
try {
InputStream inputStream =
getContentResolver().openInputStream(restoreFileUri);
restoreDatabaseThread(inputStream);
} catch (FileNotFoundException e) {
SnackBar.error(
ctx,
findViewById(android.R.id.content),
getString(R.string.restoreError));
}
}
});
private void restoreDatabaseThread(InputStream inputStream) {
Thread restoreDatabaseThread =
new Thread(
() -> {
boolean exceptionOccurred = false;
try {
String tempDir = getTempDir(ctx).getPath();
unzip(inputStream, tempDir);
checkpointIfWALEnabled(ctx, DATABASE_NAME);
restoreDatabaseFile(ctx, tempDir, DATABASE_NAME);
UserAccountsApi userAccountsApi =
BaseApi.getInstance(ctx, UserAccountsApi.class);
assert userAccountsApi != null;
UserAccount account = userAccountsApi.getAccountById(1);
AppUtil.switchToAccount(ctx, account);
} catch (final Exception e) {
exceptionOccurred = true;
SnackBar.error(
ctx,
findViewById(android.R.id.content),
getString(R.string.restoreError));
} finally {
if (!exceptionOccurred) {
runOnUiThread(this::restartApp);
}
}
});
restoreDatabaseThread.setDaemon(false);
restoreDatabaseThread.start();
}
public void restoreDatabaseFile(Context context, String tempDir, String nameOfFileToRestore)
throws IOException {
File currentDbFile = new File(context.getDatabasePath(DATABASE_NAME).getPath());
File newDbFile = new File(tempDir + "/" + nameOfFileToRestore);
if (newDbFile.exists()) {
copyFile(newDbFile, currentDbFile, false);
}
}
public void restartApp() {
Intent i = ctx.getPackageManager().getLaunchIntentForPackage(ctx.getPackageName());
assert i != null;
startActivity(Intent.makeRestartActivityTask(i.getComponent()));
Runtime.getRuntime().exit(0);
}
}

View File

@ -52,18 +52,10 @@ public class MainApplication extends Application {
Notifications.createChannels(appCtx);
DynamicColors.applyToActivitiesIfAvailable(this);
}
@Override
protected void attachBaseContext(Context context) {
super.attachBaseContext(context);
tinyDB = TinyDB.getInstance(context);
if (Boolean.parseBoolean(
AppDatabaseSettings.getSettingsValue(
context, AppDatabaseSettings.APP_CRASH_REPORTS_KEY))) {
getApplicationContext(), AppDatabaseSettings.APP_CRASH_REPORTS_KEY))) {
CoreConfigurationBuilder ACRABuilder = new CoreConfigurationBuilder();
@ -91,7 +83,7 @@ public class MainApplication extends Application {
getResources()
.getString(
R.string.crashReportEmailSubject,
AppUtil.getAppBuildNo(context)))
AppUtil.getAppBuildNo(getApplicationContext())))
.withReportAsFile(true)
.build());
@ -102,6 +94,14 @@ public class MainApplication extends Application {
}
}
@Override
protected void attachBaseContext(Context context) {
super.attachBaseContext(context);
tinyDB = TinyDB.getInstance(context);
}
public boolean switchToAccount(UserAccount userAccount, boolean tmp) {
if (!tmp || tinyDB.getInt("currentActiveAccountId") != userAccount.getAccountId()) {
currentAccount = new AccountContext(userAccount);

View File

@ -279,7 +279,11 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment {
if (issue.getIssueType().equalsIgnoreCase("issue")) {
binding.issuePrDivider.setVisibility(View.GONE);
}
} else if (!canPush) {
}
if (isRepoAdmin || canPush) {
binding.addRemoveAssignees.setVisibility(View.VISIBLE);
binding.editLabels.setVisibility(View.VISIBLE);
} else {
binding.addRemoveAssignees.setVisibility(View.GONE);
binding.editLabels.setVisibility(View.GONE);
}

View File

@ -16,6 +16,7 @@ import org.mian.gitnex.R;
import org.mian.gitnex.activities.BaseActivity;
import org.mian.gitnex.activities.MainActivity;
import org.mian.gitnex.activities.SettingsAppearanceActivity;
import org.mian.gitnex.activities.SettingsBackupRestoreActivity;
import org.mian.gitnex.activities.SettingsCodeEditorActivity;
import org.mian.gitnex.activities.SettingsGeneralActivity;
import org.mian.gitnex.activities.SettingsNotificationsActivity;
@ -44,16 +45,14 @@ public class SettingsFragment extends Fragment {
FragmentSettingsBinding.inflate(inflater, container, false);
ctx = getContext();
assert ctx != null;
materialAlertDialogBuilder =
new MaterialAlertDialogBuilder(ctx, R.style.ThemeOverlay_Material3_Dialog_Alert);
((MainActivity) requireActivity())
.setActionBarTitle(getResources().getString(R.string.navSettings));
if (((BaseActivity) requireActivity()).getAccount().requiresVersion("1.12.3")) {
fragmentSettingsBinding.notificationsFrame.setVisibility(View.VISIBLE);
}
fragmentSettingsBinding.notificationsFrame.setVisibility(View.VISIBLE);
fragmentSettingsBinding.generalFrame.setOnClickListener(
generalFrameCall -> startActivity(new Intent(ctx, SettingsGeneralActivity.class)));
@ -70,6 +69,14 @@ public class SettingsFragment extends Fragment {
fragmentSettingsBinding.notificationsFrame.setOnClickListener(
v1 -> startActivity(new Intent(ctx, SettingsNotificationsActivity.class)));
fragmentSettingsBinding.backupData.setText(
getString(
R.string.backupRestore,
getString(R.string.backup),
getString(R.string.restore)));
fragmentSettingsBinding.backupFrame.setOnClickListener(
v1 -> startActivity(new Intent(ctx, SettingsBackupRestoreActivity.class)));
fragmentSettingsBinding.rateAppFrame.setOnClickListener(rateApp -> rateThisApp());
fragmentSettingsBinding.aboutAppFrame.setOnClickListener(aboutApp -> showAboutAppDialog());
@ -102,36 +109,32 @@ public class SettingsFragment extends Fragment {
((BaseActivity) requireActivity()).getAccount().getServerVersion().toString());
aboutAppDialogBinding.donationLinkPatreon.setOnClickListener(
v12 -> {
AppUtil.openUrlInBrowser(
requireContext(),
getResources().getString(R.string.supportLinkPatreon));
});
v12 ->
AppUtil.openUrlInBrowser(
requireContext(),
getResources().getString(R.string.supportLinkPatreon)));
aboutAppDialogBinding.donationLinkBuyMeaCoffee.setOnClickListener(
v11 -> {
AppUtil.openUrlInBrowser(
requireContext(),
getResources().getString(R.string.supportLinkBuyMeaCoffee));
});
v11 ->
AppUtil.openUrlInBrowser(
requireContext(),
getResources().getString(R.string.supportLinkBuyMeaCoffee)));
aboutAppDialogBinding.translateLink.setOnClickListener(
v13 -> {
AppUtil.openUrlInBrowser(
requireContext(), getResources().getString(R.string.crowdInLink));
});
v13 ->
AppUtil.openUrlInBrowser(
requireContext(), getResources().getString(R.string.crowdInLink)));
aboutAppDialogBinding.appWebsite.setOnClickListener(
v14 -> {
AppUtil.openUrlInBrowser(
requireContext(), getResources().getString(R.string.appWebsiteLink));
});
v14 ->
AppUtil.openUrlInBrowser(
requireContext(),
getResources().getString(R.string.appWebsiteLink)));
aboutAppDialogBinding.feedback.setOnClickListener(
v14 -> {
AppUtil.openUrlInBrowser(
requireContext(), getResources().getString(R.string.feedbackLink));
});
v14 ->
AppUtil.openUrlInBrowser(
requireContext(), getResources().getString(R.string.feedbackLink)));
if (AppUtil.isPro(requireContext())) {
aboutAppDialogBinding.layoutFrame1.setVisibility(View.GONE);

View File

@ -290,7 +290,7 @@ public class AppUtil {
}
public static Boolean checkStringsWithAlphaNumeric(String str) { // [a-zA-Z0-9]
return str.matches("^[\\w]+$");
return str.matches("^\\w+$");
}
public static Boolean checkStrings(String str) { // [a-zA-Z0-9-_. ]
@ -416,7 +416,7 @@ public class AppUtil {
public static String decodeBase64(String str) {
String base64Str = str;
if (!str.equals("")) {
if (!str.isEmpty()) {
byte[] data = Base64.decode(base64Str, Base64.DEFAULT);
base64Str = new String(data, StandardCharsets.UTF_8);
}
@ -444,7 +444,7 @@ public class AppUtil {
public static long getLineCount(String s) {
if (s.length() < 1) {
if (s.isEmpty()) {
return 0;
}
@ -476,9 +476,8 @@ public class AppUtil {
.switchToAccount(userAccount, false);
}
public static boolean switchToAccount(Context context, UserAccount userAccount, boolean tmp) {
return ((MainApplication) context.getApplicationContext())
.switchToAccount(userAccount, tmp);
public static void switchToAccount(Context context, UserAccount userAccount, boolean tmp) {
((MainApplication) context.getApplicationContext()).switchToAccount(userAccount, tmp);
}
public static void sharingIntent(Context ctx, String url) {
@ -497,7 +496,7 @@ public class AppUtil {
pm.queryIntentActivities(
new Intent(intent)
.setData(
intent.getData()
Objects.requireNonNull(intent.getData())
.buildUpon()
.authority("example.com")
.scheme("https")

View File

@ -0,0 +1,209 @@
package org.mian.gitnex.helpers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
* @author M M Arif
*/
public class BackupUtil {
public static File getTempDir(Context context) {
String backupDirectoryPath = String.valueOf(context.getExternalFilesDir(null));
return new File(backupDirectoryPath);
}
public static boolean copyFile(File fromFile, File toFile, boolean bDeleteOriginalFile)
throws IOException {
boolean bSuccess = true;
FileInputStream inputStream = new FileInputStream(fromFile);
FileOutputStream outputStream = new FileOutputStream(toFile);
FileChannel fromChannel = null;
FileChannel toChannel = null;
try {
fromChannel = inputStream.getChannel();
toChannel = outputStream.getChannel();
fromChannel.transferTo(0, fromChannel.size(), toChannel);
} catch (Exception e) {
bSuccess = false;
} finally {
try {
if (fromChannel != null) {
fromChannel.close();
}
} finally {
if (toChannel != null) {
toChannel.close();
}
}
if (bDeleteOriginalFile) {
fromFile.delete();
}
}
return bSuccess;
}
public static File backupDatabaseFile(String pathOfFileToBackUp, String destinationFilePath)
throws IOException {
File currentDbFile = new File(pathOfFileToBackUp);
File newDb = new File(destinationFilePath);
if (currentDbFile.exists()) {
copyFile(currentDbFile, newDb, false);
return newDb;
}
return null;
}
public static boolean copyFileWithStreams(InputStream inputStream, OutputStream outputStream)
throws IOException {
boolean bSuccess = true;
try {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
} catch (Exception e) {
bSuccess = false;
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} finally {
if (outputStream != null) {
outputStream.close();
}
}
}
return bSuccess;
}
@SuppressLint("Recycle")
public static void checkpointIfWALEnabled(Context ctx, String DATABASE_NAME) {
Cursor csr;
int wal_busy = -99;
int wal_log = -99;
int wal_checkpointed = -99;
SQLiteDatabase db =
SQLiteDatabase.openDatabase(
ctx.getDatabasePath(DATABASE_NAME).getPath(),
null,
SQLiteDatabase.OPEN_READWRITE);
csr = db.rawQuery("PRAGMA journal_mode", null);
if (csr.moveToFirst()) {
String mode = csr.getString(0);
if (mode.equalsIgnoreCase("wal")) {
csr = db.rawQuery("PRAGMA wal_checkpoint", null);
if (csr.moveToFirst()) {
wal_busy = csr.getInt(0);
wal_log = csr.getInt(1);
wal_checkpointed = csr.getInt(2);
}
csr = db.rawQuery("PRAGMA wal_checkpoint(TRUNCATE)", null);
csr.getCount();
csr = db.rawQuery("PRAGMA wal_checkpoint", null);
if (csr.moveToFirst()) {
wal_busy = csr.getInt(0);
wal_log = csr.getInt(1);
wal_checkpointed = csr.getInt(2);
}
}
}
csr.close();
db.close();
}
public static boolean zip(
List<File> filesBeingZipped, String zipDirectory, String zipFileName) {
boolean success = true;
try {
int BUFFER = 80000;
BufferedInputStream origin;
FileOutputStream dest = new FileOutputStream(zipDirectory + "/" + zipFileName);
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));
byte[] data = new byte[BUFFER];
for (File fileBeingZipped : filesBeingZipped) {
FileInputStream fi = new FileInputStream(fileBeingZipped);
origin = new BufferedInputStream(fi, BUFFER);
ZipEntry entry =
new ZipEntry(
fileBeingZipped
.getPath()
.substring(fileBeingZipped.getPath().lastIndexOf("/") + 1));
out.putNextEntry(entry);
int count;
while ((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
origin.close();
}
out.close();
} catch (Exception e) {
success = false;
}
return success;
}
public static void unzip(InputStream uriInputStream, String unzipDirectory) throws Exception {
ZipInputStream zipInputStream = new ZipInputStream(uriInputStream);
ZipEntry ze;
while ((ze = zipInputStream.getNextEntry()) != null) {
FileOutputStream fileOutputStream =
new FileOutputStream(unzipDirectory + "/" + ze.getName());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
byte[] buffer = new byte[1024];
int read = 0;
while ((read = zipInputStream.read(buffer)) != -1) {
bufferedOutputStream.write(buffer, 0, read);
}
zipInputStream.closeEntry();
bufferedOutputStream.close();
fileOutputStream.close();
}
zipInputStream.close();
}
}

View File

@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,6c0,1.657 3.582,3 8,3s8,-1.343 8,-3s-3.582,-3 -8,-3s-8,1.343 -8,3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M4,6v6c0,1.657 3.582,3 8,3c1.118,0 2.183,-0.086 3.15,-0.241"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M20,12v-6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M4,12v6c0,1.657 3.582,3 8,3c0.157,0 0.312,-0.002 0.466,-0.005"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M16,19h6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M19,16l3,3l-3,3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
</vector>

View File

@ -256,12 +256,23 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen16dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:text="@string/btnLogin"
android:textColor="?attr/materialCardBackgroundColor"
android:letterSpacing="0.1"
android:textStyle="bold" />
<TextView
android:id="@+id/restore_from_backup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/restoreFromBackup"
android:textSize="@dimen/dimen14sp"
android:textColor="?attr/inputTextColor"
android:gravity="center"
android:layout_marginBottom="@dimen/dimen16dp"
android:layout_marginTop="@dimen/dimen8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryBackgroundColor"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryBackgroundColor"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
android:background="?attr/primaryBackgroundColor"
app:contentScrim="?attr/primaryBackgroundColor"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:elevation="0dp"
android:layout_height="?attr/actionBarSize"
app:title="@string/backup"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_close" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen16dp">
<LinearLayout
android:id="@+id/backupDataFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:layout_marginTop="@dimen/dimen8dp"
android:orientation="vertical">
<TextView
android:id="@+id/backupHeaderSelector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/backup"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/backupDataHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/backupDataHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/restoreDataFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen32dp"
android:clickable="true"
android:focusable="true"
android:orientation="vertical">
<TextView
android:id="@+id/restoreHeaderSelector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/restore"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/restoreDataHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/restoreDataHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -16,17 +16,17 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp">
android:paddingTop="@dimen/dimen10dp">
<LinearLayout
android:id="@+id/generalFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginBottom="@dimen/dimen6dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="15dp">
android:padding="@dimen/dimen16dp">
<ImageView
android:layout_width="wrap_content"
@ -37,8 +37,8 @@
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="@dimen/dimen10dp"
android:layout_marginEnd="@dimen/dimen10dp"
android:layout_weight="1"
android:orientation="vertical">
@ -46,22 +46,22 @@
android:id="@+id/tvGeneral"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/settingsGeneralHeader"
android:textColor="?attr/primaryTextColor"
android:textSize="18sp"/>
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/generalHintText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/generalHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="12sp"/>
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
@ -77,11 +77,11 @@
android:id="@+id/appearanceFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginBottom="@dimen/dimen6dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="15dp">
android:padding="@dimen/dimen16dp">
<ImageView
android:layout_width="wrap_content"
@ -92,8 +92,8 @@
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="@dimen/dimen10dp"
android:layout_marginEnd="@dimen/dimen10dp"
android:layout_weight="1"
android:orientation="vertical">
@ -101,22 +101,22 @@
android:id="@+id/tvAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/settingsAppearanceHeader"
android:textColor="?attr/primaryTextColor"
android:textSize="18sp"/>
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/appearanceHintText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/appearanceHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="12sp"/>
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
@ -132,11 +132,11 @@
android:id="@+id/codeEditorFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginBottom="@dimen/dimen6dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="15dp">
android:padding="@dimen/dimen16dp">
<ImageView
android:layout_width="wrap_content"
@ -147,8 +147,8 @@
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="@dimen/dimen10dp"
android:layout_marginEnd="@dimen/dimen10dp"
android:layout_weight="1"
android:orientation="vertical">
@ -156,22 +156,22 @@
android:id="@+id/codeEditor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/codeEditor"
android:textColor="?attr/primaryTextColor"
android:textSize="18sp"/>
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/codeEditorHintText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/codeEditorHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="12sp"/>
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
@ -179,7 +179,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_chevron_right"/>
app:srcCompat="@drawable/ic_chevron_right" />
</LinearLayout>
@ -187,23 +187,23 @@
android:id="@+id/securityFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginBottom="@dimen/dimen6dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="15dp">
android:padding="@dimen/dimen16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_security"/>
app:srcCompat="@drawable/ic_security" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="@dimen/dimen10dp"
android:layout_marginEnd="@dimen/dimen10dp"
android:layout_weight="1"
android:orientation="vertical">
@ -211,22 +211,22 @@
android:id="@+id/tvSecurity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/settingsSecurityHeader"
android:textColor="?attr/primaryTextColor"
android:textSize="18sp"/>
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/securityHintText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/securityHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="12sp"/>
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
@ -234,7 +234,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_chevron_right"/>
app:srcCompat="@drawable/ic_chevron_right" />
</LinearLayout>
@ -242,23 +242,23 @@
android:id="@+id/notificationsFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginBottom="@dimen/dimen6dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="15dp">
android:padding="@dimen/dimen16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_notifications"/>
app:srcCompat="@drawable/ic_notifications" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="@dimen/dimen10dp"
android:layout_marginEnd="@dimen/dimen10dp"
android:layout_weight="1"
android:orientation="vertical">
@ -266,22 +266,22 @@
android:id="@+id/notificationsHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/pageTitleNotifications"
android:textColor="?attr/primaryTextColor"
android:textSize="18sp"/>
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/notificationsHintText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/notificationsHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="12sp"/>
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
@ -289,7 +289,62 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_chevron_right"/>
app:srcCompat="@drawable/ic_chevron_right" />
</LinearLayout>
<LinearLayout
android:id="@+id/backupFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen6dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="@dimen/dimen16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_export" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dimen10dp"
android:layout_marginEnd="@dimen/dimen10dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/backupData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/backup"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/backupDataHintText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/backupRestoreHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_chevron_right" />
</LinearLayout>
@ -297,23 +352,23 @@
android:id="@+id/rateAppFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginBottom="@dimen/dimen6dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="15dp">
android:padding="@dimen/dimen16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_like"/>
app:srcCompat="@drawable/ic_like" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginStart="@dimen/dimen10dp"
android:layout_marginEnd="@dimen/dimen10dp"
android:layout_weight="1"
android:orientation="vertical">
@ -321,22 +376,22 @@
android:id="@+id/rateApp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/navRate"
android:textColor="?attr/primaryTextColor"
android:textSize="18sp"/>
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/rateAppHintText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingStart="@dimen/dimen12dp"
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/rateAppHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="12sp"/>
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
@ -356,7 +411,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_info"/>
app:srcCompat="@drawable/ic_info" />
<LinearLayout
android:layout_width="0dp"
@ -374,7 +429,7 @@
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/navAbout"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen18sp"/>
android:textSize="@dimen/dimen18sp" />
<TextView
android:id="@+id/aboutAppHintText"
@ -385,7 +440,7 @@
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/aboutAppHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen12sp"/>
android:textSize="@dimen/dimen12sp" />
</LinearLayout>
@ -405,7 +460,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/generalImgContentText"
app:srcCompat="@drawable/ic_logout"/>
app:srcCompat="@drawable/ic_logout" />
<LinearLayout
android:layout_width="0dp"
@ -423,7 +478,7 @@
android:paddingEnd="@dimen/dimen12dp"
android:text="@string/navLogout"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen18sp"/>
android:textSize="@dimen/dimen18sp" />
</LinearLayout>

View File

@ -262,6 +262,16 @@
<string name="fragmentTabsAnimationHeader">Tabs Animation</string>
<string name="fadeOut">Fade Out</string>
<string name="zoomOut">Zoom Out</string>
<string name="backupRestore" translatable="false">%1$s / %2$s</string>
<string name="backup">Backup</string>
<string name="restore">Restore</string>
<string name="backupFileSuccess">The backup was successfully created</string>
<string name="backupFileError">An error occurred while creating a data backup</string>
<string name="backupFilePopupText">This action will backup your accounts, settings, drafts, notes, and data related to repositories.\n\nClick the Backup button to download the backup file.</string>
<string name="restoreFilePopupText">You are about to restore from a GitNex-generated backup file. This will restore user accounts, settings, drafts, notes, and data related to repositories.\n\nPlease note this action will overwrite the current database data. Proceed with caution.\n\nClick Restore to start the process.</string>
<string name="restoreError">An error occurred while restoring from the backup file.</string>
<string name="restoreFromBackup">Restore from Backup</string>
<string name="restoreFromBackupPopupText">You are about to restore from a GitNex-generated backup file. This will restore user accounts, settings, drafts, notes, and data related to repositories.\n\nClick Restore to start the process.</string>
<!-- settings -->
<string name="noMoreData">No more data available</string>
@ -647,6 +657,9 @@
<string name="rateAppHintText">If you like GitNex you can give it a thumbs up</string>
<string name="aboutAppHintText">App version, build, user instance version</string>
<string name="codeEditorHintText">Syntax color, indentation</string>
<string name="backupRestoreHintText">Backup and restore accounts, settings and more</string>
<string name="backupDataHintText">Backup accounts, settings, notes and more</string>
<string name="restoreDataHintText">Restore accounts, settings, notes and more</string>
<string name="archivedRepository">Archived</string>
<string name="archivedRepositoryMessage">This repo is archived. You can view files, but cannot push or open issues/pull-requests.</string>