Java tutorial
/** * RxDroid - A Medication Reminder * Copyright (C) 2011-2014 Joseph Lehner <joseph.c.lehner@gmail.com> * * * RxDroid 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. Additional terms apply (see LICENSE). * * RxDroid 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 RxDroid. If not, see <http://www.gnu.org/licenses/>. * * */ package at.jclehner.rxdroid; import android.Manifest; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Build; import android.os.Environment; import android.os.Message; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Base64; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import net.lingala.zip4j.core.ZipFile; import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.util.Zip4jConstants; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import at.jclehner.androidutils.StorageHelper; import at.jclehner.rxdroid.db.Database; import at.jclehner.rxdroid.db.DatabaseHelper; import at.jclehner.rxdroid.util.Util; import at.jclehner.rxdroid.util.WrappedCheckedException; public class Backup { private static final String TAG = Backup.class.getSimpleName(); private static final String DIRECTORY_NAME = "RxDroid" + (BuildConfig.DEBUG ? "Dbg" : ""); public static final File DIRECTORY = new File(Environment.getExternalStorageDirectory(), DIRECTORY_NAME); public static abstract class StorageStateListener extends BroadcastReceiver { private static final IntentFilter INTENT_FILTER = new IntentFilter(); private boolean mReadable; private boolean mWritable; public StorageStateListener(Context context) { update(context, getStorageState()); } @Override public final void onReceive(Context context, Intent intent) { final String storageState = getStorageState(); update(context, storageState); onStateChanged(storageState, intent); } public void register(Context context) { context.registerReceiver(this, INTENT_FILTER); } public void unregister(Context context) { context.unregisterReceiver(this); } public abstract void onStateChanged(String storageState, Intent intent); public boolean isReadable() { return mReadable; } public boolean isWritable() { return mWritable; } public static boolean isReadable(String storageState) { return Environment.MEDIA_MOUNTED_READ_ONLY.equals(storageState) || Environment.MEDIA_MOUNTED.equals(storageState); } public static boolean isWritable(String storageState) { return Environment.MEDIA_MOUNTED.equals(storageState); } public void update(Context context) { update(context, Environment.getExternalStorageState()); } private void update(Context context, String storageState) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { mReadable = mWritable = false; } else { mReadable = isReadable(storageState); mWritable = isWritable(storageState); } } static { INTENT_FILTER.addAction(Intent.ACTION_MEDIA_MOUNTED); INTENT_FILTER.addAction(Intent.ACTION_MEDIA_UNMOUNTED); INTENT_FILTER.addAction(Intent.ACTION_MEDIA_EJECT); INTENT_FILTER.addAction(Intent.ACTION_MEDIA_REMOVED); } } public static class BackupFile { private ZipFile mZip = null; private String mPath; private String[] mInfo; private Date mTimestamp; private int mVersion; private int mDbVersion; private boolean mIsEncrypted; public BackupFile(String path) { mPath = path; try { mZip = new ZipFile(path); mIsEncrypted = mZip.isEncrypted(); if (mZip.getComment() == null) return; mInfo = mZip.getComment().split(":"); } catch (ZipException e) { Log.w(TAG, e); return; } if (mInfo.length < 2 || !mInfo[0].startsWith("rxdbak") || mInfo[0].equals("rxdbak")) return; mVersion = Integer.parseInt(mInfo[0].substring("rxdbak".length())); mTimestamp = new Date(Long.parseLong(mInfo[1])); if (mInfo.length >= 3) mDbVersion = Integer.parseInt(mInfo[2].substring("DBv".length())); else mDbVersion = -1; } public boolean isValid() { return mZip != null && mVersion == 1; } public boolean isEncrypted() { return mIsEncrypted; } public int version() { return mVersion; } public int dbVersion() { return mDbVersion; } public String getPath() { return mPath; } public Date getTimestamp() { return mTimestamp; } public String getLocation() { final String file = new File(mPath).getAbsolutePath(); final String filesDir = RxDroid.getContext().getFilesDir().getAbsolutePath(); if (file.startsWith(filesDir)) return file.replace(filesDir, "[files]"); return StorageHelper.getPrettyName(file, RxDroid.getContext(), null); } public ZipFile getZip() { return mZip; } public boolean restore(String password) { if (!isValid()) throw new IllegalStateException("Invalid backup file"); synchronized (Database.LOCK_DATA) { final String key = Settings.getString(Settings.Keys.BACKUP_KEY, ""); try { if (password != null) mZip.setPassword(password); mZip.extractAll(RxDroid.getPackageInfo().applicationInfo.dataDir); } catch (ZipException e) { final String msg = e.getMessage(); if (password != null && msg.toLowerCase(Locale.US).contains("password")) return false; throw new WrappedCheckedException(e); } Settings.init(true); Settings.putString(Settings.Keys.BACKUP_KEY, key); } NotificationReceiver.rescheduleAlarmsAndUpdateNotification(false); return true; } } public static String getStorageState() { final String state; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) state = Environment.getStorageState(DIRECTORY); else state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state) && !DIRECTORY.canWrite()) { Log.d(TAG, "Storage state reported as MEDIA_MOUNTED, but " + DIRECTORY + " is not writeable"); return Environment.MEDIA_MOUNTED_READ_ONLY; } return state; } public static File makeBackupFilename(String template) { return new File(DIRECTORY + "/" + template + ".rxdbak"); } public static File createBackup(File outFile, String password) throws ZipException { return createBackup(outFile, password, RxDroid.getPackageInfo().applicationInfo.dataDir, -1); } public static File createBackup(File outFile, String password, String dataDir, long time) throws ZipException { if (outFile == null) { final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss"); outFile = makeBackupFilename(sdf.format(new Date())); } synchronized (Database.LOCK_DATA) { final ZipFile zip = new ZipFile(outFile); for (int i = 0; i != FILES.length; ++i) { final File file = new File(dataDir, FILES[i]); if (!file.exists()) continue; final ZipParameters zp = new ZipParameters(); zp.setRootFolderInZip(new File(FILES[i]).getParent()); zp.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL); zp.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); if (!TextUtils.isEmpty(password)) { zp.setPassword(password); zp.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES); zp.setAesKeyStrength(Zip4jConstants.AES_STRENGTH_256); zp.setEncryptFiles(true); //zp.setCompressionMethod(Zip4jConstants.COMP_AES_ENC); } zip.addFile(file, zp); } if (time == -1) time = System.currentTimeMillis(); zip.setComment("rxdbak1:" + time + ":DBv" + DatabaseHelper.DB_VERSION); } return outFile; } public static List<File> getBackupDirectories(Context context) { final List<File> dirs = new ArrayList<>(); dirs.add(context.getFilesDir()); for (StorageHelper.PathInfo si : StorageHelper.getDirectories(context)) dirs.add(new File(si.path, DIRECTORY_NAME)); return dirs; } public static List<File> getBackupFiles(Context context) { final List<File> files = new ArrayList<>(); for (File dir : getBackupDirectories(context)) { if (!dir.exists() || !dir.isDirectory()) continue; final File[] dirFiles = dir.listFiles(FILTER); if (dirFiles != null) { for (File file : dirFiles) { if (file.isFile()) files.add(file); } } } return files; } private static void encrypt(Context context, File backup, String password) throws ZipException, IOException { final BackupFile bf = new BackupFile(backup.getAbsolutePath()); if (!bf.isValid() || bf.isEncrypted()) return; Log.i(TAG, "Encrypting " + backup); final File tmpDir = new File(context.getCacheDir(), "tmp"); tmpDir.mkdirs(); final File tmpFile = new File(tmpDir, "tmp.rxdbak"); bf.getZip().extractAll(tmpDir.getAbsolutePath()); tmpFile.delete(); createBackup(tmpFile, password, tmpDir.getAbsolutePath(), bf.getTimestamp().getTime()); Util.copyFile(tmpFile, backup); } private static void encryptAll(final Context context, final String key, final Runnable callback) { new AsyncTask<Void, String, Exception>() { private ProgressDialog mDialog; private List<File> mFiles; @Override protected void onPreExecute() { mFiles = Backup.getBackupFiles(context); mDialog = new ProgressDialog(context); mDialog.setTitle(R.string._msg_encrypting); mDialog.setCancelable(false); mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); mDialog.setIndeterminate(false); mDialog.setMax(mFiles.size()); mDialog.setProgress(0); mDialog.show(); } @Override protected Exception doInBackground(Void... params) { int progress = 0; for (File file : mFiles) { try { Backup.encrypt(context, file, key); } catch (IOException | ZipException e) { Log.w(TAG, e); return e; } mDialog.setProgress(progress++); } return null; } @Override protected void onCancelled() { super.onCancelled(); } @Override protected void onPostExecute(Exception e) { if (mDialog != null) { mDialog.dismiss(); mDialog = null; } if (e != null) Util.showExceptionDialog(mDialog.getContext(), e); else if (callback != null) callback.run(); } }.execute(); } private static String passwordToKey(String password) { if (password.length() == 0) return null; try { final MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update("RxDroid".getBytes()); return Base64.encodeToString(md.digest(password.getBytes("UTF-8")), Base64.NO_WRAP); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { throw new WrappedCheckedException(e); } } public static class PasswordDialog extends AlertDialog implements DialogInterface.OnShowListener, TextWatcher, View.OnClickListener { private final Context mContext; private final boolean mCreateBackup; private Button mPosBtn; private TextView mMessage; private EditText mPw; private EditText mPwRepeat; private CheckBox mUseForAll; private String mOldKey; private Runnable mBackupCallback; public PasswordDialog(Context context) { this(context, false); } public PasswordDialog(Context context, boolean createBackup) { super(context); mContext = context; mCreateBackup = createBackup; setView(getLayoutInflater().inflate(R.layout.dialog_pw, null)); setButton(BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), (Message) null); setButton(BUTTON_POSITIVE, mContext.getString(android.R.string.ok), (Message) null); setOnShowListener(this); } public void setBackupSuccessCallback(Runnable callback) { mBackupCallback = callback; } @Override public void onClick(View v) { if (v != mPosBtn) return; final String pw = mPw.getText().toString(); if (pw.equals(mPwRepeat.getText().toString())) { final String key = !pw.equals(mOldKey) ? passwordToKey(pw) : mOldKey; if (key != null && mUseForAll.isChecked()) Settings.putString(Settings.Keys.BACKUP_KEY, key); else Settings.remove(Settings.Keys.BACKUP_KEY); if (mCreateBackup) { try { Backup.createBackup(null, key); if (mBackupCallback != null) mBackupCallback.run(); } catch (ZipException e) { Util.showExceptionDialog(mContext, e); } } else if (key != null) { encryptAll(mContext, key, mBackupCallback); } dismiss(); } else { mPwRepeat.setText(""); mPwRepeat.setError(mContext.getText(R.string._msg_pw_error)); } } @Override public void onShow(DialogInterface dialog) { if (dialog != this) return; final boolean isInitialSetting = !Settings.contains(Settings.Keys.BACKUP_KEY); mOldKey = Settings.getString(Settings.Keys.BACKUP_KEY, ""); mPosBtn = getButton(BUTTON_POSITIVE); mPosBtn.setOnClickListener(this); mPosBtn.setEnabled(!isInitialSetting || mCreateBackup); mPw = (EditText) findViewById(R.id.pw_new); // It doesn't really matter what we put here. The idea is that if a password is // already set, we allow the user to remove the password protection by setting // a zero-length password mPw.setText(mOldKey); mPw.addTextChangedListener(this); mPwRepeat = (EditText) findViewById(R.id.pw_repeat); mPwRepeat.setText(mOldKey); mPwRepeat.addTextChangedListener(this); mUseForAll = (CheckBox) findViewById(R.id.checkbox); mMessage = (TextView) findViewById(R.id.message); if (!mCreateBackup) { mMessage.setText(R.string._msg_change_backup_pw); mUseForAll.setChecked(true); mUseForAll.setEnabled(false); } else mUseForAll.setChecked(true); } @Override public void afterTextChanged(Editable s) { mPwRepeat.setError(null); mPosBtn.setEnabled(mPw.length() == mPwRepeat.length()); } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } } private static final FilenameFilter FILTER = new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.endsWith(".rxdbak"); } }; private static final String[] FILES = { "databases/" + DatabaseHelper.DB_NAME, "shared_prefs/at.jclehner.rxdroid" + (BuildConfig.DEBUG ? ".debug" : "") + "_preferences.xml", "shared_prefs/showcase_internal.xml" }; }