/*
* Copyright (c) 2015, Nils Braden
*
* This file is part of ttrss-reader-fork. 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 (at your option) 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 org.ttrssreader.utils;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Vibrator;
import android.support.v4.net.ConnectivityManagerCompat;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Toast;
import org.ttrssreader.R;
import org.ttrssreader.controllers.Controller;
import org.ttrssreader.preferences.Constants;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Set;
import java.util.regex.Pattern;
public class Utils {
private static final String TAG = Utils.class.getSimpleName();
public static final long SECOND = 1000;
public static final long MINUTE = 60 * SECOND;
public static final long HOUR = 60 * MINUTE;
public static final long DAY = 24 * HOUR;
public static final long KB = 1024;
public static final long MB = KB * KB;
/**
* The maximum number of articles to store.
*/
public static final int ARTICLE_LIMIT = 5000;
/**
* Vibrate-Time for vibration when end of list is reached
*/
public static final long SHORT_VIBRATE = 50;
/**
* The time after which data will be fetched again from the server if asked for the data
*/
public static final long UPDATE_TIME = MINUTE * 30;
/**
* The time after which the DB and other data will be cleaned up again,
*/
public static final long CLEANUP_TIME = DAY;
/**
* The Pattern to match image-urls inside HTML img-tags.
*/
public static final Pattern findImageUrlsPattern = Pattern
.compile("<(?:img|video)[^>]+?src=[\"']([^\"']*)", Pattern.CASE_INSENSITIVE);
private static final int ID_RUNNING = 4564561;
private static final int ID_FINISHED = 7897891;
/**
* Different network states
*/
public static final int NETWORK_NONE = 0;
public static final int NETWORK_MOBILE = 1;
public static final int NETWORK_METERED = 2;
public static final int NETWORK_WIFI = 3;
/*
* Check if this is the first run of the app.
*/
public static boolean checkIsFirstRun() {
if (Controller.getInstance().isFirstRun()) {
// Set first run to false anyway.
Controller.getInstance().setFirstRun(false);
// Compatibility for already installed apps that don't have this pref yet:
return Constants.LAST_VERSION_RUN_DEFAULT.equals(Controller.getInstance().getLastVersionRun());
} else {
return false;
}
}
/*
* Check if a new version of the app was installed, returns true if this is the case. This also triggers the reset
* of the preference noCrashreportsUntilUpdate since with a new update the crash reporting should now be enabled
* again.
*/
public static boolean checkIsNewVersion(Context c) {
String thisVersion = getAppVersionName(c);
String lastVersionRun = Controller.getInstance().getLastVersionRun();
Controller.getInstance().setLastVersionRun(thisVersion);
if (thisVersion.equals(lastVersionRun)) {
// No new version installed, perhaps a new version exists
// Only run task once for every session and only if we are online
if (!checkConnected((ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE)))
return false;
if (AsyncTask.Status.PENDING.equals(updateVersionTask.getStatus()))
updateVersionTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return false;
} else {
// New update was installed, reset noCrashreportsUntilUpdate and return true to display the changelog...
Controller.getInstance().setNoCrashreportsUntilUpdate(false);
return true;
}
}
/*
* Checks the config for a user-defined server, returns true if the config is invalid and the user has not yet
* entered a valid server adress.
*/
public static boolean checkIsConfigInvalid() {
try {
URI uri = Controller.getInstance().uri();
if (uri == null || uri.toASCIIString().equals(Constants.URL_DEFAULT + Controller.JSON_END_URL)) {
return true;
}
} catch (URISyntaxException e) {
return true;
}
return false;
}
/**
* Retrieves the packaged version-code of the application
*
* @param c - The Activity to retrieve the current version
* @return the version-string
*/
public static int getAppVersionCode(Context c) {
int result;
try {
PackageManager manager = c.getPackageManager();
PackageInfo info = manager.getPackageInfo(c.getPackageName(), 0);
result = info.versionCode;
} catch (NameNotFoundException e) {
Log.w(TAG, "Unable to get application version: " + e.getMessage());
result = 0;
}
return result;
}
/**
* Retrieves the packaged version-name of the application
*
* @param c - The Activity to retrieve the current version
* @return the version-string
*/
public static String getAppVersionName(Context c) {
String result;
try {
PackageManager manager = c.getPackageManager();
PackageInfo info = manager.getPackageInfo(c.getPackageName(), 0);
result = info.versionName;
} catch (NameNotFoundException e) {
Log.w(TAG, "Unable to get application version: " + e.getMessage());
result = "";
}
return result;
}
/**
* Checks if the option to work offline is set or if the data-connection isn't established, else returns true. If
* we are about to connect it waits for maximum one second and then returns the network state without waiting
* anymore.
*/
public static boolean isConnected(ConnectivityManager cm) {
return !Controller.getInstance().workOffline() && checkConnected(cm);
}
/**
* Wrapper for Method checkConnected(ConnectivityManager cm, boolean onlyWifi)
*/
public static boolean checkConnected(ConnectivityManager cm) {
return checkConnected(cm, Controller.getInstance().onlyUseWifi(), false);
}
/**
* Only checks the connectivity without regard to the preferences
*/
public static boolean checkConnected(ConnectivityManager cm, boolean onlyWifi, boolean onlyUnmeteredNetwork) {
if (cm == null) return false;
NetworkInfo info = cm.getActiveNetworkInfo();
if (info != null && info.isConnected()) {
if (onlyWifi && info.getType() != ConnectivityManager.TYPE_WIFI) {
return false;
}
if (onlyUnmeteredNetwork) {
return !isNetworkMetered(cm);
}
return true;
}
return false;
}
public static int getNetworkType(final ConnectivityManager cm) {
if (cm == null) return NETWORK_NONE;
final NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || !info.isConnected()) {
return NETWORK_NONE;
} else if (info.getType() != ConnectivityManager.TYPE_WIFI) {
return NETWORK_MOBILE;
} else if (isNetworkMetered(cm)) {
return NETWORK_METERED;
} else {
return NETWORK_WIFI;
}
}
private static boolean isNetworkMetered(ConnectivityManager cm) {
if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
return ConnectivityManagerCompat.isActiveNetworkMetered(cm);
else
return cm.isActiveNetworkMetered();
}
/**
* Allos to send a toast from a background thread
*
* @param context the context, eg. MyApplication.context()
* @param message like Toast.makeText(...)
* @param length like Toast.makeText(...)
*/
public static void showBackgroundToast(final Context context, final String message, final int length) {
Handler handler = new Handler(context.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, message, length).show();
}
});
}
public static void showFinishedNotification(String content, int time, boolean error, Context context) {
showFinishedNotification(content, time, error, context, new Intent());
}
/**
* Shows a notification with the given parameters
*
* @param content the string to display
* @param time how long the process took
* @param error set to true if an error occured
* @param context the context
*/
public static void showFinishedNotification(String content, int time, boolean error, Context context,
Intent intent) {
if (context == null) return;
NotificationManager mNotMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
int icon = R.drawable.icon;
CharSequence title = String.format((String) context.getText(R.string.Utils_DownloadFinishedTitle), time);
CharSequence ticker = context.getText(R.string.Utils_DownloadFinishedTicker);
CharSequence text = content;
if (content == null) text = context.getText(R.string.Utils_DownloadFinishedText);
if (error) {
icon = R.drawable.icon;
title = context.getText(R.string.Utils_DownloadErrorTitle);
ticker = context.getText(R.string.Utils_DownloadErrorTicker);
}
Notification notification = buildNotification(context, icon, ticker, title, text, true, intent);
mNotMan.notify(ID_FINISHED, notification);
}
public static void showRunningNotification(Context context, boolean finished) {
showRunningNotification(context, finished, new Intent());
}
/**
* Shows a notification indicating that something is running. When called with finished=true it removes the
* notification.
*
* @param context the context
* @param finished if the notification is to be removed
*/
private static void showRunningNotification(Context context, boolean finished, Intent intent) {
if (context == null) return;
NotificationManager mNotMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// if finished remove notification and return, else display notification
if (finished) {
mNotMan.cancel(ID_RUNNING);
return;
}
int icon = R.drawable.notification_icon;
CharSequence title = context.getText(R.string.Utils_DownloadRunningTitle);
CharSequence ticker = context.getText(R.string.Utils_DownloadRunningTicker);
Notification notification = buildNotification(context, icon, ticker, title, "…", true, intent);
mNotMan.notify(ID_RUNNING, notification);
}
/**
* Reads a file from my webserver and parses the content. It containts the version code of the latest supported
* version. If the version of the installed app is lower then this the feature "Send mail with stacktrace on error"
* will be disabled to make sure I only receive "new" Bugreports.
*/
private static AsyncTask<Void, Void, Void> updateVersionTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// Check last appVersionCheckDate
long last = Controller.getInstance().appVersionCheckTime();
if ((System.currentTimeMillis() - last) < (Utils.HOUR * 4)) return null;
if (Controller.getInstance().isNoCrashreports()) return null;
try {
URL url = new URL("http://nilsbraden.de/android/tt-rss/minSupportedVersion.txt");
HttpURLConnection con = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
con.connect();
int code = con.getResponseCode();
if (code < 400 || code >= 600) {
BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
String content = br.readLine(); // Just read one line!
// Only ever read the integer if it matches the regex and is not too long
if (content.matches("[0-9]*[\\r\\n]*")) {
content = content.replaceAll("[^0-9]*", "");
Controller.getInstance().setAppLatestVersion(Integer.parseInt(content));
}
}
} catch (Exception e) {
// Empty!
}
return null;
}
};
@SuppressWarnings("deprecation")
public static Notification buildNotification(Context context, int icon, CharSequence ticker, CharSequence title,
CharSequence text, boolean autoCancel, Intent intent) {
Notification notification = null;
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
try {
Notification.Builder builder = new Notification.Builder(context);
builder.setSmallIcon(icon);
builder.setTicker(ticker);
builder.setWhen(System.currentTimeMillis());
builder.setContentTitle(title);
builder.setContentText(text);
builder.setContentIntent(pendingIntent);
builder.setAutoCancel(autoCancel);
if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
notification = builder.getNotification();
else notification = builder.build();
} catch (Exception re) {
Log.e(TAG, "Exception while building notification. Does your device propagate the right API-Level? ("
+ Build.VERSION.SDK_INT + ")", re);
}
return notification;
}
public static String separateItems(Set<?> att, String separator) {
if (att == null) return "";
String ret;
StringBuilder sb = new StringBuilder();
for (Object s : att) {
sb.append(s);
sb.append(separator);
}
if (att.size() > 0) {
ret = sb.substring(0, sb.length() - separator.length());
} else {
ret = sb.toString();
}
return ret;
}
private static final String REGEX_URL = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]";
public static boolean validateURL(String url) {
return url != null && url.matches(REGEX_URL);
}
public static String getTextFromClipboard(Context context) {
// New Clipboard API
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard.hasPrimaryClip()) {
if (!clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN))
return null;
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
CharSequence chars = item.getText();
if (chars != null && chars.length() > 0) {
return chars.toString();
} else {
Uri pasteUri = item.getUri();
if (pasteUri != null) {
return pasteUri.toString();
}
}
}
return null;
}
public static boolean clipboardHasText(Context context) {
return (getTextFromClipboard(context) != null);
}
public static void alert(Activity activity) {
alert(activity, false);
}
/**
* Alert the user by a short vibration or a flash of the whole screen.
*/
public static void alert(Activity activity, boolean error) {
Vibrator vib = ((Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE));
if (vib.hasVibrator()) {
vib.vibrate(Utils.SHORT_VIBRATE);
} else if (error) {
// Only flash when user tried to move forward, flashing when reaching the last article looks just wrong.
Animation flash = AnimationUtils.loadAnimation(activity, R.anim.flash);
View main = activity.findViewById(R.id.frame_all);
main.startAnimation(flash);
}
}
}