/*
* Copyright 2015 Qianqian Zhu <zhuqianqian.299@gmail.com> All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.z299studio.pb;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Random;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
public class Application{
private static final String DATA_FILE = "data";
private static final int APP_VERSION = 2;
private static Application __instance;
SharedPreferences mSP;
private SharedPreferences mFpData;
public static class Options {
static boolean mAlwaysShowPwd;
static int mAutoLock;
static boolean mEnableCopyPwd;
static int mFpStatus;
static boolean mShowOther;
static int mSync;
static boolean mSyncMsg;
static int mSyncVersion;
static int mTheme;
static boolean mTour;
static boolean mWarnCopyPwd;
static Date mSyncTime;
}
static class FileHeader {
public int version; // 1 byte
int iterationCount; // 1 byte
int keyLength; //1 byte
int ivLength; // 1 byte
int revision; // 10 bytes
public int size;
boolean valid;
static final int HEADER_SIZE = 16;
static final int RESERVED = 10;
static FileHeader parse(byte[] buffer) {
FileHeader fh = new FileHeader();
fh.valid = false;
if(buffer!=null) {
if(buffer[0] == 0x50 && buffer[1] == 0x42) {
int i = 2;
fh.valid = true;
fh.version = buffer[i++];
fh.iterationCount = 100 * (buffer[i++]);
fh.keyLength = buffer[i++];
fh.ivLength = buffer[i++];
fh.revision = Integer.parseInt(new String(buffer, i, RESERVED).trim());
i += RESERVED;
fh.size = i;
}
}
return fh;
}
public static byte[] build(int version, int count, int keyLength, int ivLength, int revision) {
byte[] header = new byte[HEADER_SIZE];
int i = 0;
header[i++] = 0x50; header[i++] = 0x42;
header[i++] = (byte) version;
header[i++] = (byte) (count / 100);
header[i++] = (byte) keyLength;
header[i++] = (byte) ivLength;
byte[] revBytes = String.format(Locale.ENGLISH, "%10d", revision).getBytes();
System.arraycopy(revBytes, 0, header, i, revBytes.length);
// i += RESERVED;
return header;
}
}
private byte[] mBuffer;
private int mDataSize;
private FileHeader mFileHeader;
private long mLastPause;
private boolean mIgnoreNextPause;
private boolean mTimedOut;
private String mPassword;
private int mLocalVersion;
private Crypto mCrypto;
private Hashtable<Integer, Boolean> mChanges;
private AccountManager mAccountManager;
static final Integer THEME = 0;
static final Integer DATA_OTHER = 1;
static final Integer DATA_ALL = 2;
public static Application getInstance(Activity context) {
if(__instance == null) {
__instance = new Application(context);
}
return __instance;
}
public static Application getInstance() {
return __instance;
}
private Application(Context context) {
mSP = PreferenceManager.getDefaultSharedPreferences(context);
mFpData = context.getSharedPreferences(C.Fingerprint.FILE, Context.MODE_PRIVATE);
Options.mTheme = mSP.getInt(C.Keys.THEME, 2);
Options.mTour = mSP.getBoolean(C.Keys.TOUR, false);
mChanges = new Hashtable<>();
}
void onStart() {
Options.mAutoLock = mSP.getInt(C.Keys.AUTO_LOCK_TIME, -1);
if(Options.mAutoLock == -1) {
boolean autolock_v1 = mSP.getBoolean(C.Keys.AUTO_LOCK, false);
Options.mAutoLock = autolock_v1 ? 1000 : 0;
}
Options.mAlwaysShowPwd = mSP.getBoolean(C.Keys.SHOW_PWD, false);
Options.mEnableCopyPwd = mSP.getBoolean(C.Keys.ENABLE_COPY, true);
Options.mShowOther = mSP.getBoolean(C.Keys.SHOW_OTHER, false);
Options.mSync = mSP.getInt(C.Sync.SERVER, C.Sync.NONE);
Options.mSyncMsg = mSP.getBoolean(C.Sync.MSG, true);
Options.mSyncVersion = mSP.getInt(C.Sync.VERSION, 0);
Options.mWarnCopyPwd = mSP.getBoolean(C.Keys.WARN_COPY, true);
String syncTime = mSP.getString(C.Sync.TIME, "0000-00-00 00:00:00");
DateFormat df = DateFormat.getDateTimeInstance();
try {
Options.mSyncTime = df.parse(syncTime);
}
catch (ParseException e) {
Options.mSyncTime = new Date(0L);
}
}
public AccountManager getAccountManager() {
return mAccountManager;
}
void setAccountManager(AccountManager mgr, int imgCode, String defCategoryName) {
mAccountManager = mgr;
mAccountManager.setDefaultCategory(imgCode, defCategoryName);
}
void setCrypto(Crypto crypto) {
mCrypto = crypto;
}
boolean queryChange(int what) {
return mChanges.get(what) != null;
}
void notifyChange(int what) {
mChanges.put(what, Boolean.TRUE);
if(what == DATA_ALL || what == DATA_OTHER) {
reset();
}
}
void handleChange(int what) {
mChanges.remove(what);
}
boolean hasDataFile(Context context) {
boolean success = false;
try {
File file = new File(context.getFilesDir()+"/"+DATA_FILE);
mDataSize = (int) file.length();
if(mDataSize > 0) {
success = true;
}
} catch (Exception e) {
e.printStackTrace();
}
return success;
}
byte[] getData(Context context) {
if(mBuffer==null) {
try {
if(mDataSize < 1) {
saveData(context);
hasDataFile(context);
}
if(mDataSize > 0) {
mBuffer = new byte[mDataSize];
FileInputStream fis = context.openFileInput(DATA_FILE);
fis.read(mBuffer, 0, mDataSize);
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return mBuffer;
}
FileHeader getAppHeaderData(Context context) {
if(mBuffer == null) {
getData(context);
}
if(mBuffer!=null) {
mFileHeader = FileHeader.parse(mBuffer);
mLocalVersion = mFileHeader.revision;
}
return mFileHeader;
}
int getLocalVersion() {
int version = 0;
if (mFileHeader != null) {
version = mFileHeader.revision;
}
return version;
}
public String getPassword() {
return mPassword;
}
void setPassword(String password, boolean reset) {
mPassword = password;
if(reset) {
mCrypto = new Crypto();
mCrypto.resetPassword(password);
}
}
void increaseVersion(Context context, int atLeast) {
if(mLocalVersion <= atLeast) {
mLocalVersion = atLeast + 1;
saveData(context);
}
}
void saveData(Context context) {
if(mLocalVersion <= Options.mSyncVersion) {
mLocalVersion++;
}
byte[] cipher = mCrypto.encrypt(mAccountManager.getBytes());
byte[] header = FileHeader.build(APP_VERSION, mCrypto.getIterationCount(),
Crypto.SALT_LENGTH, mCrypto.getIvLength(), mLocalVersion);
byte[] keyInfo = mCrypto.getSaltAndIvBytes();
try {
FileOutputStream fos = context.openFileOutput(DATA_FILE, Context.MODE_PRIVATE);
fos.write(header);
fos.write(keyInfo);
fos.write(cipher);
int size = header.length + keyInfo.length + cipher.length;
if (mBuffer == null || mBuffer.length != size) {
mBuffer = new byte[size];
}
System.arraycopy(header, 0, mBuffer, 0, header.length);
System.arraycopy(keyInfo, 0, mBuffer, header.length, keyInfo.length);
System.arraycopy(cipher, 0, mBuffer, header.length + keyInfo.length, cipher.length);
fos.close();
mFileHeader = FileHeader.parse(mBuffer);
mAccountManager.onSaved();
}catch (FileNotFoundException e) {
Log.w("Passbook", "File not found");
}
catch(IOException ioe) {
Log.e("Passbook", "IOException");
}
}
void onPause(Context context) {
if(mAccountManager.saveRequired()) {
saveData(context);
}
mLastPause = System.currentTimeMillis();
}
boolean needAuth() {
if(mTimedOut) {
mTimedOut = false;
return true;
}
if(mIgnoreNextPause || Options.mAutoLock < 1) {
mIgnoreNextPause = false;
return false;
}
return (System.currentTimeMillis() - mLastPause) > Options.mAutoLock;
}
void ignoreNextPause() {
mIgnoreNextPause = true;
}
void setTimedOut() { mTimedOut = true; }
void saveData(Context context, byte[] data, FileHeader fileHeader) {
try {
mLocalVersion = fileHeader.revision;
mFileHeader = fileHeader;
mBuffer = data;
FileOutputStream fos = context.openFileOutput(DATA_FILE, Context.MODE_PRIVATE);
fos.write(data);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
void onVersionUpdated(int revision) {
Options.mSyncVersion = revision;
mSP.edit().putInt(C.Sync.VERSION, revision).apply();
}
String onSyncSucceed() {
Options.mSyncTime = new Date();
DateFormat df = DateFormat.getDateTimeInstance();
String time = df.format(Options.mSyncTime);
mSP.edit().putString(C.Sync.TIME, time).apply();
return time;
}
int queryFpStatus() {
Options.mFpStatus = mFpData.getInt(C.Fingerprint.STATUS, C.Fingerprint.UNKNOWN);
return Options.mFpStatus;
}
byte[] getFpIv() {
return Base64.decode(mFpData.getString(C.Fingerprint.IV, "1234"), Base64.DEFAULT);
}
byte[] getFpData() {
return Base64.decode(mFpData.getString(C.Fingerprint.DATA, "1234"), Base64.DEFAULT);
}
void setFpData(byte[] data, byte[] iv) {
mFpData.edit()
.putInt(C.Fingerprint.STATUS, C.Fingerprint.ENABLED)
.putString(C.Fingerprint.DATA, Base64.encodeToString(data, Base64.DEFAULT))
.putString(C.Fingerprint.IV, Base64.encodeToString(iv, Base64.DEFAULT))
.apply();
Options.mFpStatus = C.Fingerprint.ENABLED;
}
void clearFpData() {
mFpData.edit().clear()
.putInt(C.Fingerprint.STATUS, C.Fingerprint.DISABLED)
.apply();
Options.mFpStatus = C.Fingerprint.DISABLED;
}
void resetFpData() {
mFpData.edit().clear().apply();
Options.mFpStatus = C.Fingerprint.UNKNOWN;
}
static AccountManager decrypt(Crypto crypto, String password,
FileHeader header, byte[]data)
throws GeneralSecurityException{
if(data!=null) {
int total = header.keyLength + header.ivLength;
crypto.setPassword(password, data, header.size, total);
total += header.size;
byte[] textData = new byte[data.length - total];
System.arraycopy(data, total, textData, 0, textData.length);
byte[] text = crypto.decrypt(textData);
Log.i("Passbook", "Decrypt complete successfully.");
return new AccountManager(new String(text));
}
return null;
}
static void showToast(Activity context, int stringId, int duration) {
if(android.os.Build.VERSION.SDK_INT>=android.os.Build.VERSION_CODES.KITKAT) {
Toast.makeText(context, stringId, duration).show();
}
else {
LayoutInflater inflater = context.getLayoutInflater();
View layout = inflater.inflate(R.layout.toast,
(ViewGroup)context.findViewById(R.id.toast_layout_root));
TextView desc = (TextView)layout.findViewById(R.id.description);
desc.setText(stringId);
Toast toast = new Toast(context.getApplicationContext());
toast.setView(layout);
toast.setDuration(duration);
toast.show();
}
}
static void showToast(Activity context, String text, int duration) {
if(android.os.Build.VERSION.SDK_INT>=android.os.Build.VERSION_CODES.KITKAT) {
Toast.makeText(context, text, duration).show();
}
else {
LayoutInflater inflater = context.getLayoutInflater();
View layout = inflater.inflate(R.layout.toast,
(ViewGroup)context.findViewById(R.id.toast_layout_root));
TextView desc = (TextView)layout.findViewById(R.id.description);
desc.setText(text);
Toast toast = new Toast(context.getApplicationContext());
toast.setView(layout);
toast.setDuration(duration);
toast.show();
}
}
private static int[] __icons__= {
R.drawable.pb_bank, R.drawable.pb_creditcard, R.drawable.pb_desktop,
R.drawable.pb_shop, R.drawable.pb_email, R.drawable.pb_web,
R.drawable.pb_wallet, R.drawable.pb_atm, R.drawable.pb_bag,
R.drawable.pb_gift, R.drawable.pb_school, R.drawable.pb_folder,
R.drawable.pb_briefcase, R.drawable.pb_chat, R.drawable.pb_lock,
R.drawable.pb_user
};
static int[] getThemedIcons() {
return __icons__;
}
private static String[] sCategoryNames;
private static int[] sCategoryIcons;
private static int[] sCategoryIds;
String[] getSortedCategoryNames() {
if(sCategoryNames == null) {
int size;
ArrayList<AccountManager.Category> categories =
mAccountManager.getCategoryList(false, true);
size = categories.size() + 1;
sCategoryNames = new String[size];
sCategoryIds = new int[size];
sCategoryIcons = new int[size];
int i = 0;
AccountManager.Category defaultCategory =
mAccountManager.getCategory(AccountManager.DEFAULT_CATEGORY_ID);
sCategoryNames[i] = defaultCategory.mName;
sCategoryIds[i] = defaultCategory.mId;
sCategoryIcons[i++] = defaultCategory.mImgCode;
for(AccountManager.Category category : categories) {
sCategoryNames[i] = category.mName;
sCategoryIds[i] = category.mId;
sCategoryIcons[i++] = category.mImgCode;
}
}
return sCategoryNames;
}
int[] getSortedCategoryIds() {
if(sCategoryNames == null) {
getSortedCategoryNames();
}
return sCategoryIds;
}
int[] getSortedCategoryIcons() {
if(sCategoryNames == null) {
getSortedCategoryNames();
}
return sCategoryIcons;
}
static void reset() { sCategoryNames = null; }
private static Random random = new Random();
private static char[] candidates = new char[96];
static String generate(boolean hasA2Z, boolean has_a2z, boolean hasDigits,
boolean hasChars, boolean hasSpace, int minLen, int maxLen) {
if(!hasA2Z && !has_a2z && !hasDigits && !hasChars && !hasSpace) {
return "";
}
int length;
if(minLen == maxLen) {
length = minLen;
}
else {
int min = minLen > maxLen ? maxLen : minLen;
int max = minLen > maxLen ? minLen : maxLen;
length = min;
length += random.nextInt(max-min+1);
}
int index = 0, i;
char[] specChars = {'~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_',
'=', '+', '[', '{', ']', '}', '\\', '|', ';', ':', '\'', '"', '<', ',', '.', '>',
'/', '?', ' '};
char result[] = new char[length];
if(hasA2Z) {
for(i = 0; i < 26; ++i) {
candidates[index++] = (char) ('A' + i) ;
}
}
if(has_a2z) {
for(i = 0; i < 26; ++i) {
candidates[index++] = (char)('a'+i);
}
}
if(hasDigits) {
for(i = 0; i < 10; ++i) {
candidates[index++] = (char)('0' + i);
}
}
if(hasChars) {
System.arraycopy(specChars, 0, candidates, index, specChars.length);
index += specChars.length;
}
if(hasSpace) {
candidates[index++] = 0x20;
}
for(i = 0; i < length; ++i) {
result[i] = candidates[random.nextInt(index)];
}
return String.valueOf(result);
}
}