/*
* MaxLock, an Xposed applock module for Android
* Copyright (C) 2014-2016 Max Rumpf alias Maxr1998
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.Maxr1998.xposed.maxlock.util;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.WallpaperManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.BitmapFactory;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import org.apache.commons.io.FileUtils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.ref.SoftReference;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import de.Maxr1998.xposed.maxlock.Common;
import de.Maxr1998.xposed.maxlock.R;
import de.Maxr1998.xposed.maxlock.ui.actions.ActionActivity;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static de.Maxr1998.xposed.maxlock.util.MLPreferences.getPreferences;
import static de.Maxr1998.xposed.maxlock.util.MLPreferences.getPreferencesKeys;
import static de.Maxr1998.xposed.maxlock.util.MLPreferences.getPreferencesKeysPerApp;
public final class Util {
public static final int PATTERN_CODE = 48;
public static final int PATTERN_CODE_APP = 5;
public static final String LOG_TAG = "MaxLock";
public static final String LOG_TAG_STARTUP = "ML-Startup";
public static final String LOG_TAG_LOCKSCREEN = "ML-Lockscreen";
public static final String LOG_TAG_TASKER = "ML-Tasker";
public static final String LOG_TAG_ADMIN = "ML-DeviceAdmin";
public static final String LOG_TAG_IAB = "ML-IAB";
private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
private static SoftReference<Drawable> WALLPAPER = new SoftReference<>(null);
private Util() {}
// UI
/**
* This method calculates a size in pixels from a given dp value.
*
* @param o Object of either View or Context
* @param dp Value to convert to pixels
* @return Calculated Pixels
*/
public static int dpToPx(@NonNull Object o, int dp) {
Context c;
if (o instanceof View) {
c = ((View) o).getContext();
} else if (o instanceof Context) {
c = (Context) o;
} else {
throw new IllegalArgumentException("This object only takes views or contexts as argument!");
}
return (int) (c.getResources().getDisplayMetrics().density * dp);
}
public static void setTheme(Activity a) {
if (!getPreferences(a).getBoolean(Common.USE_DARK_STYLE, false)) {
a.setTheme(R.style.AppTheme);
} else {
if (!getPreferences(a).getBoolean(Common.USE_AMOLED_BLACK, false)) {
a.setTheme(R.style.AppTheme_Dark);
} else {
a.setTheme(R.style.AppTheme_Dark_AMOLED);
}
}
}
/**
* Hide keyboard from window
*
* @param a Activity to call from
* @param v View where keyboard is attached
* @return true if keyboard got hidden, false if the keyboard wasn't visible before
*/
public static boolean hideKeyboardFromWindow(Activity a, View v) {
ResultReceiver result = new ResultReceiver(null) {
private int i = -1;
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
i = resultCode;
}
@Override
public int describeContents() {
return i;
}
};
a.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
//noinspection ConstantConditions
((InputMethodManager) a.getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(v.getWindowToken(), 0, result);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return result.describeContents() == InputMethodManager.RESULT_HIDDEN;
}
public static void getBackground(AppCompatActivity activity, final ImageView background) {
switch (getPreferences(background.getContext()).getString(Common.BACKGROUND, "")) {
case "color":
background.setImageDrawable(new ColorDrawable(getPreferences(background.getContext()).getInt(Common.BACKGROUND_COLOR, ContextCompat.getColor(background.getContext(), R.color.accent))));
break;
case "custom":
try {
background.setImageBitmap(BitmapFactory.decodeStream(background.getContext().openFileInput("background")));
} catch (IOException | OutOfMemoryError e) {
background.setImageDrawable(new ColorDrawable(ContextCompat.getColor(background.getContext(), R.color.accent)));
Toast.makeText(background.getContext(), "Error loading background image, " + (e instanceof IOException ? ", IOException." : "is it to big?"), Toast.LENGTH_LONG).show();
}
break;
default:
activity.getSupportLoaderManager().initLoader(0, null, new LoaderManager.LoaderCallbacks<Drawable>() {
@Override
public Loader<Drawable> onCreateLoader(int id, Bundle args) {
return new AsyncTaskLoader<Drawable>(background.getContext()) {
@Override
protected void onStartLoading() {
super.onStartLoading();
forceLoad();
}
@Override
public Drawable loadInBackground() {
if (WALLPAPER.get() != null)
return WALLPAPER.get();
else
return WallpaperManager.getInstance(getContext()).getFastDrawable();
}
};
}
@Override
public void onLoadFinished(Loader<Drawable> loader, Drawable data) {
if (data != null) {
background.setImageDrawable(data);
if (WALLPAPER.get() == null)
WALLPAPER = new SoftReference<>(data);
} else {
background.setImageDrawable(new ColorDrawable(ContextCompat.getColor(background.getContext(), R.color.accent)));
Toast.makeText(background.getContext(), "Failed to load system wallpaper!", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onLoaderReset(Loader<Drawable> loader) {
}
});
break;
}
}
// Lock
public static String shaHash(String toHash) { // from: [ http://stackoverflow.com/a/11978976 ]. Thanks very much!
String hash = null;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] bytes = toHash.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
bytes = digest.digest();
hash = bytesToHex(bytes);
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
}
return hash;
}
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static void receiveAndSetPattern(Context context, char[] pattern, String app) {
StringBuilder patternKey = new StringBuilder();
for (char x : pattern) {
patternKey.append(x);
}
if (app == null) {
getPreferences(context).edit().putString(Common.LOCKING_TYPE, Common.PREF_VALUE_PATTERN).apply();
getPreferencesKeys(context).edit().putString(Common.KEY_PREFERENCE, Util.shaHash(patternKey.toString())).apply();
} else {
getPreferencesKeysPerApp(context).edit().putString(app, Common.PREF_VALUE_PATTERN).putString(app + Common.APP_KEY_PREFERENCE, Util.shaHash(patternKey.toString())).apply();
}
}
public static int getPatternCode(int app) {
if (app == -1) {
return PATTERN_CODE;
} else {
int code = Integer.valueOf(String.valueOf(PATTERN_CODE_APP) + String.valueOf(app));
System.out.println(code);
return code;
}
}
public static void setPassword(final Context context, final String app) {
@SuppressLint("InflateParams") final View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_set_password, null);
final AlertDialog dialog = new AlertDialog.Builder(context)
.setCancelable(false)
.setTitle(R.string.pref_set_password)
.setView(dialogView)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.show();
((ViewGroup) dialogView.getParent()).setPadding(10, 10, 10, 10);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
EditText p1 = (EditText) dialogView.findViewById(R.id.edt_password);
EditText p2 = (EditText) dialogView.findViewById(R.id.edt_re_password);
String v1 = p1.getText().toString();
String v2 = p2.getText().toString();
if (!v1.equals(v2)) {
p1.setText("");
p2.setText("");
Toast.makeText(context, R.string.toast_password_inconsistent, Toast.LENGTH_SHORT)
.show();
} else if (v1.length() == 0) {
Toast.makeText(context, R.string.toast_password_null, Toast.LENGTH_SHORT)
.show();
} else {
dialog.dismiss();
if (app == null) {
getPreferencesKeys(context).edit().putString(Common.KEY_PREFERENCE, shaHash(v1)).apply();
getPreferences(context).edit().putString(Common.LOCKING_TYPE, v1.matches("[0-9]+") ? Common.PREF_VALUE_PASS_PIN : Common.PREF_VALUE_PASSWORD).apply();
} else {
getPreferencesKeysPerApp(context).edit().putString(app, v1.matches("[0-9]+") ? Common.PREF_VALUE_PASS_PIN : Common.PREF_VALUE_PASSWORD).putString(app + Common.APP_KEY_PREFERENCE, shaHash(v1)).apply();
}
Toast.makeText(context, R.string.toast_password_changed, Toast.LENGTH_SHORT).show();
}
});
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> dialog.dismiss());
}
public static void logFailedAuthentication(Context context, String pkg) {
String toLog = "[" + new SimpleDateFormat("dd/MM/yy, HH:mm:ss", Locale.getDefault()).format(new Date(System.currentTimeMillis())) + "] " + getApplicationNameFromPackage(pkg, context);
PrintWriter writer = null;
try {
writer = new PrintWriter(new BufferedWriter(new FileWriter(context.getApplicationInfo().dataDir + File.separator + Common.LOG_FILE, true)));
} catch (IOException e) {
e.printStackTrace();
}
if (writer != null) {
writer.printf("%s" + "%n", toLog);
writer.close();
}
}
// Packages
public static String dataDir(Context context) {
return context.getApplicationInfo().dataDir + File.separator;
}
public static void checkForStoragePermission(final Fragment fragment, final int code, @StringRes int description) {
if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
new AlertDialog.Builder(fragment.getActivity())
.setMessage(description)
.setPositiveButton(android.R.string.ok, (dialog, which) -> fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, code))
.setNegativeButton(android.R.string.cancel, null)
.create().show();
} else {
fragment.onRequestPermissionsResult(code, new String[]{WRITE_EXTERNAL_STORAGE}, new int[]{PackageManager.PERMISSION_GRANTED});
}
}
public static String getApplicationNameFromPackage(String packageName, Context context) {
if (context instanceof ActionActivity) {
return packageName;
}
PackageManager packageManager = context.getPackageManager();
try {
CharSequence label = packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, 0));
if (label != null) {
return label.toString();
} else {
return packageName;
}
} catch (PackageManager.NameNotFoundException e) {
return "(Not found)";
}
}
public static Drawable getApplicationIconFromPackage(String packageName, Context context) {
try {
return context.getPackageManager().getApplicationIcon(packageName);
} catch (PackageManager.NameNotFoundException e) {
return ContextCompat.getDrawable(context, R.mipmap.ic_launcher);
}
}
public static String getLanguageCode() {
String language = Locale.getDefault().getLanguage();
String country = Locale.getDefault().getCountry();
if (language.equals("")) {
return "en-GB";
}
if (!country.equals("")) {
return language + "-" + country;
} else return language;
}
public static boolean isDevMode() {
try {
BufferedReader r = new BufferedReader(new FileReader(Common.EXTERNAL_FILES_DIR + "dev_mode.key"));
return Util.shaHash(r.readLine()).toLowerCase().equals("08b49da56ef8f5bf0aa51c64d5e683ba3e7599bd6e2e3906e584fca14cb95f82");
} catch (Exception e) {
return false;
}
}
/**
* Compresses files from a directory into a zip file.
*
* @param directory the directory to compress
* @param stream the ZipOutputStream to write the files to
* @throws IOException
*/
public static void writeDirectoryToZip(File directory, ZipOutputStream stream) throws IOException {
writeDirectoryToZip(directory, stream, directory);
}
private static void writeDirectoryToZip(File d, ZipOutputStream s, File t) throws IOException {
for (File f : d.listFiles()) {
if (f.isDirectory()) {
writeDirectoryToZip(f, s, t);
continue;
}
String path = f.getAbsolutePath().replace(t.getAbsolutePath(), "");
ZipEntry entry = new ZipEntry(path);
s.putNextEntry(entry);
FileUtils.copyFile(f, s);
s.closeEntry();
}
}
}