Fix backups
This commit is contained in:
parent
41cf364438
commit
494f9d66fc
@ -0,0 +1,65 @@
|
|||||||
|
package com.cringe_studios.cringe_authenticator.backup;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
|
||||||
|
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
|
||||||
|
import com.cringe_studios.cringe_authenticator.model.OTPData;
|
||||||
|
import com.cringe_studios.cringe_authenticator.proto.OTPMigration;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.BackupException;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
public class BackupData {
|
||||||
|
|
||||||
|
private CryptoParameters parameters;
|
||||||
|
|
||||||
|
private String database;
|
||||||
|
|
||||||
|
private BackupGroup[] groups;
|
||||||
|
|
||||||
|
private BackupData() {}
|
||||||
|
|
||||||
|
public BackupData(CryptoParameters parameters, String database, BackupGroup[] groups) {
|
||||||
|
this.parameters = parameters;
|
||||||
|
this.database = database;
|
||||||
|
this.groups = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CryptoParameters getParameters() {
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDatabase() {
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OTPDatabase loadDatabase(SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException {
|
||||||
|
try {
|
||||||
|
return OTPDatabase.loadFromEncryptedBytes(Base64.decode(database, Base64.DEFAULT), key, parameters);
|
||||||
|
}catch(IllegalArgumentException e) {
|
||||||
|
throw new BackupException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackupGroup[] getGroups() {
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
if(parameters == null || database == null || groups == null) return false;
|
||||||
|
|
||||||
|
if(!parameters.isValid()) return false;
|
||||||
|
|
||||||
|
for(BackupGroup group : groups) {
|
||||||
|
if(!group.isValid()) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package com.cringe_studios.cringe_authenticator.backup;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
|
public class BackupGroup {
|
||||||
|
|
||||||
|
public String id;
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
private BackupGroup() {}
|
||||||
|
|
||||||
|
public BackupGroup(String id, String name) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return id != null && name != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package com.cringe_studios.cringe_authenticator.util;
|
package com.cringe_studios.cringe_authenticator.backup;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -6,10 +6,18 @@ import android.util.Base64;
|
|||||||
|
|
||||||
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
|
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
|
||||||
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
|
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.BackupException;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.IOUtil;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException;
|
||||||
|
import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonSyntaxException;
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@ -17,6 +25,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
@ -33,12 +42,20 @@ public class BackupUtil {
|
|||||||
if(!OTPDatabase.isDatabaseLoaded()) throw new BackupException("Database is not loaded");
|
if(!OTPDatabase.isDatabaseLoaded()) throw new BackupException("Database is not loaded");
|
||||||
|
|
||||||
byte[] dbBytes = OTPDatabase.convertToEncryptedBytes(OTPDatabase.getLoadedDatabase(), key, parameters);
|
byte[] dbBytes = OTPDatabase.convertToEncryptedBytes(OTPDatabase.getLoadedDatabase(), key, parameters);
|
||||||
JsonObject object = new JsonObject();
|
String database = Base64.encodeToString(dbBytes, Base64.DEFAULT);
|
||||||
object.add("parameters", SettingsUtil.GSON.toJsonTree(parameters));
|
|
||||||
object.addProperty("database", Base64.encodeToString(dbBytes, Base64.DEFAULT));
|
List<String> groups = SettingsUtil.getGroups(context);
|
||||||
|
BackupGroup[] groupsArray = new BackupGroup[groups.size()];
|
||||||
|
for(int i = 0; i < groups.size(); i++) {
|
||||||
|
String group = groups.get(i);
|
||||||
|
groupsArray[i] = new BackupGroup(group, SettingsUtil.getGroupName(context, group));
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupData data = new BackupData(parameters, database, groupsArray);
|
||||||
|
|
||||||
try(OutputStream out = context.getContentResolver().openOutputStream(backupFile)) {
|
try(OutputStream out = context.getContentResolver().openOutputStream(backupFile)) {
|
||||||
if(out == null) throw new BackupException("Failed to write backup");
|
if(out == null) throw new BackupException("Failed to write backup");
|
||||||
out.write(object.toString().getBytes(StandardCharsets.UTF_8));
|
out.write(SettingsUtil.GSON.toJson(data).getBytes(StandardCharsets.UTF_8));
|
||||||
}catch(IOException e) {
|
}catch(IOException e) {
|
||||||
throw new BackupException(e);
|
throw new BackupException(e);
|
||||||
}
|
}
|
||||||
@ -48,21 +65,23 @@ public class BackupUtil {
|
|||||||
try(InputStream in = context.getContentResolver().openInputStream(backupFile)) {
|
try(InputStream in = context.getContentResolver().openInputStream(backupFile)) {
|
||||||
if(in == null) throw new BackupException("Failed to read backup file");
|
if(in == null) throw new BackupException("Failed to read backup file");
|
||||||
byte[] backupBytes = IOUtil.readBytes(in);
|
byte[] backupBytes = IOUtil.readBytes(in);
|
||||||
JsonObject object = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), JsonObject.class);
|
BackupData data = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), BackupData.class);
|
||||||
return SettingsUtil.GSON.fromJson(object.get("parameters"), CryptoParameters.class); // TODO: check if params are valid
|
if(!data.getParameters().isValid()) throw new BackupException("Invalid crypto parameters");
|
||||||
|
return data.getParameters();
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
throw new BackupException("Invalid JSON", e);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new BackupException(e);
|
throw new BackupException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OTPDatabase loadBackup(Context context, Uri backupFile, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException {
|
public static BackupData loadBackup(Context context, Uri backupFile) throws BackupException {
|
||||||
try (InputStream in = context.getContentResolver().openInputStream(backupFile)) {
|
try (InputStream in = context.getContentResolver().openInputStream(backupFile)) {
|
||||||
if (in == null) throw new BackupException("Failed to read backup file");
|
if (in == null) throw new BackupException("Failed to read backup file");
|
||||||
byte[] backupBytes = IOUtil.readBytes(in);
|
byte[] backupBytes = IOUtil.readBytes(in);
|
||||||
JsonObject object = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), JsonObject.class);
|
BackupData data = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), BackupData.class);
|
||||||
JsonElement db = object.get("database");
|
if(!data.isValid()) throw new BackupException("Invalid backup data"); // TODO: more info on backup errors
|
||||||
if(db == null) throw new BackupException("Invalid backup file");
|
return data;
|
||||||
return OTPDatabase.loadFromEncryptedBytes(Base64.decode(db.getAsString(), Base64.DEFAULT), key, parameters);
|
|
||||||
} catch (JsonSyntaxException e) {
|
} catch (JsonSyntaxException e) {
|
||||||
throw new BackupException("Invalid JSON", e);
|
throw new BackupException("Invalid JSON", e);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
@ -75,6 +75,19 @@ public class CryptoParameters {
|
|||||||
return encryptionSaltLength;
|
return encryptionSaltLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return hashType != null
|
||||||
|
&& argon2Version > 0
|
||||||
|
&& argon2Iterations > 0
|
||||||
|
&& argon2Memory > 0
|
||||||
|
&& argon2Parallelism > 0
|
||||||
|
&& encryptionAlgorithm != null
|
||||||
|
&& encryptionGCMTagLength > 0
|
||||||
|
&& encryptionIVLength > 0
|
||||||
|
&& encryptionAESKeyLength > 0
|
||||||
|
&& encryptionSaltLength > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public static CryptoParameters createNew() {
|
public static CryptoParameters createNew() {
|
||||||
CryptoParameters params = new CryptoParameters();
|
CryptoParameters params = new CryptoParameters();
|
||||||
byte[] salt = Crypto.generateSalt(params);
|
byte[] salt = Crypto.generateSalt(params);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package com.cringe_studios.cringe_authenticator.fragment;
|
package com.cringe_studios.cringe_authenticator.fragment;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -15,6 +14,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import com.cringe_studios.cringe_authenticator.MainActivity;
|
import com.cringe_studios.cringe_authenticator.MainActivity;
|
||||||
import com.cringe_studios.cringe_authenticator.R;
|
import com.cringe_studios.cringe_authenticator.R;
|
||||||
|
import com.cringe_studios.cringe_authenticator.backup.BackupData;
|
||||||
import com.cringe_studios.cringe_authenticator.crypto.BiometricKey;
|
import com.cringe_studios.cringe_authenticator.crypto.BiometricKey;
|
||||||
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
|
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
|
||||||
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
|
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
|
||||||
@ -22,7 +22,7 @@ import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
|
|||||||
import com.cringe_studios.cringe_authenticator.databinding.FragmentSettingsBinding;
|
import com.cringe_studios.cringe_authenticator.databinding.FragmentSettingsBinding;
|
||||||
import com.cringe_studios.cringe_authenticator.util.Appearance;
|
import com.cringe_studios.cringe_authenticator.util.Appearance;
|
||||||
import com.cringe_studios.cringe_authenticator.util.BackupException;
|
import com.cringe_studios.cringe_authenticator.util.BackupException;
|
||||||
import com.cringe_studios.cringe_authenticator.util.BackupUtil;
|
import com.cringe_studios.cringe_authenticator.backup.BackupUtil;
|
||||||
import com.cringe_studios.cringe_authenticator.util.BiometricUtil;
|
import com.cringe_studios.cringe_authenticator.util.BiometricUtil;
|
||||||
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
|
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
|
||||||
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
|
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
|
||||||
@ -31,9 +31,6 @@ import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
|
|||||||
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
|
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
|
||||||
import com.cringe_studios.cringe_authenticator.util.Theme;
|
import com.cringe_studios.cringe_authenticator.util.Theme;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@ -265,11 +262,18 @@ public class SettingsFragment extends NamedFragment {
|
|||||||
|
|
||||||
private void loadBackup(Uri uri) {
|
private void loadBackup(Uri uri) {
|
||||||
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
|
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
|
||||||
|
if(SettingsUtil.isDatabaseEncrypted(requireContext())) {
|
||||||
try {
|
try {
|
||||||
SecretKey key = OTPDatabase.getLoadedKey();
|
SecretKey key = OTPDatabase.getLoadedKey();
|
||||||
CryptoParameters parameters = SettingsUtil.getCryptoParameters(requireContext());
|
CryptoParameters parameters = SettingsUtil.getCryptoParameters(requireContext());
|
||||||
loadBackup(uri, key, parameters);
|
loadBackup(uri, key, parameters);
|
||||||
} catch (CryptoException e) {
|
} catch (CryptoException ignored) { // Load with password
|
||||||
|
} catch (BackupException | OTPDatabaseException e) {
|
||||||
|
DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DialogUtil.showInputPasswordDialog(requireContext(), password -> {
|
DialogUtil.showInputPasswordDialog(requireContext(), password -> {
|
||||||
try {
|
try {
|
||||||
CryptoParameters parameters = BackupUtil.loadParametersFromBackup(requireContext(), uri);
|
CryptoParameters parameters = BackupUtil.loadParametersFromBackup(requireContext(), uri);
|
||||||
@ -279,15 +283,25 @@ public class SettingsFragment extends NamedFragment {
|
|||||||
DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e2);
|
DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e2);
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
} catch(BackupException | OTPDatabaseException e) {
|
|
||||||
DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e);
|
|
||||||
}
|
|
||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadBackup(Uri uri, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException {
|
private void loadBackup(Uri uri, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException {
|
||||||
OTPDatabase db = BackupUtil.loadBackup(requireContext(), uri, key, parameters);
|
BackupData data = BackupUtil.loadBackup(requireContext(), uri);
|
||||||
DialogUtil.showErrorDialog(requireContext(), "Success: " + db);
|
OTPDatabase db = data.loadDatabase(key, parameters);
|
||||||
|
//DialogUtil.showErrorDialog(requireContext(), "Success: " + db);
|
||||||
|
// TODO: prompt user that all current data will be deleted
|
||||||
|
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
|
||||||
|
OTPDatabase oldDatabase = OTPDatabase.getLoadedDatabase();
|
||||||
|
try {
|
||||||
|
SettingsUtil.restoreGroups(requireContext(), data.getGroups());
|
||||||
|
OTPDatabase.setLoadedDatabase(db);
|
||||||
|
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
|
||||||
|
} catch (OTPDatabaseException | CryptoException e) {
|
||||||
|
OTPDatabase.setLoadedDatabase(oldDatabase);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -166,6 +166,10 @@ public class OTPDatabase {
|
|||||||
loadedDatabase = null;
|
loadedDatabase = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setLoadedDatabase(OTPDatabase loadedDatabase) {
|
||||||
|
OTPDatabase.loadedDatabase = loadedDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
public static OTPDatabase getLoadedDatabase() {
|
public static OTPDatabase getLoadedDatabase() {
|
||||||
return loadedDatabase;
|
return loadedDatabase;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import android.content.SharedPreferences;
|
|||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import com.cringe_studios.cringe_authenticator.R;
|
import com.cringe_studios.cringe_authenticator.R;
|
||||||
|
import com.cringe_studios.cringe_authenticator.backup.BackupGroup;
|
||||||
import com.cringe_studios.cringe_authenticator.crypto.BiometricKey;
|
import com.cringe_studios.cringe_authenticator.crypto.BiometricKey;
|
||||||
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
|
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
|
||||||
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
|
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
|
||||||
@ -42,6 +43,19 @@ public class SettingsUtil {
|
|||||||
prefs.edit().putString("groups", GSON.toJson(groups)).apply();
|
prefs.edit().putString("groups", GSON.toJson(groups)).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void restoreGroups(Context ctx, BackupGroup[] groups) {
|
||||||
|
List<String> oldGroups = getGroups(ctx);
|
||||||
|
for(String group : oldGroups) removeGroup(ctx, group);
|
||||||
|
|
||||||
|
List<String> newGroups = new ArrayList<>();
|
||||||
|
for(BackupGroup group : groups) {
|
||||||
|
newGroups.add(group.getId());
|
||||||
|
setGroupName(ctx, group.getId(), group.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroups(ctx, newGroups);
|
||||||
|
}
|
||||||
|
|
||||||
public static void addGroup(Context ctx, String group, String groupName) {
|
public static void addGroup(Context ctx, String group, String groupName) {
|
||||||
List<String> groups = new ArrayList<>(getGroups(ctx));
|
List<String> groups = new ArrayList<>(getGroups(ctx));
|
||||||
groups.add(group);
|
groups.add(group);
|
||||||
@ -62,28 +76,6 @@ public class SettingsUtil {
|
|||||||
deleteGroupData(ctx, group);
|
deleteGroupData(ctx, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*public static List<OTPData> getOTPs(Context ctx, String group) {
|
|
||||||
String currentOTPs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".otps", "[]");
|
|
||||||
return Arrays.asList(GSON.fromJson(currentOTPs, OTPData[].class));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addOTP(Context ctx, String group, @NonNull OTPData data) {
|
|
||||||
// TODO: check for code with same name
|
|
||||||
|
|
||||||
List<OTPData> otps = new ArrayList<>(getOTPs(ctx, group));
|
|
||||||
otps.add(data);
|
|
||||||
|
|
||||||
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
|
||||||
.putString("group." + group + ".otps", GSON.toJson(otps.toArray(new OTPData[0])))
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void updateOTPs(Context ctx, String group, List<OTPData> otps) {
|
|
||||||
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
|
||||||
.putString("group." + group + ".otps", GSON.toJson(otps.toArray(new OTPData[0])))
|
|
||||||
.apply();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
public static String getGroupName(Context ctx, String group) {
|
public static String getGroupName(Context ctx, String group) {
|
||||||
return ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".name", group);
|
return ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".name", group);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user