package mono.hg.utils; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.net.Uri; import android.os.Build; import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; import android.widget.EditText; import android.widget.Toast; import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.appcompat.app.AppCompatActivity; import java.io.Closeable; import java.io.IOException; import java.lang.annotation.Retention; import java.util.ArrayList; import mono.hg.LauncherActivity; import mono.hg.R; import mono.hg.fragments.WidgetsDialogFragment; import mono.hg.helpers.PreferenceHelper; import mono.hg.models.WebSearchProvider; import mono.hg.receivers.PackageChangesReceiver; import static java.lang.annotation.RetentionPolicy.SOURCE; public class Utils { /** * Sends log using a predefined tag. This is used to better debug or to catch errors. * Logging should always use sendLog to coalesce logs into one single place. * * @param level Urgency level of the log to send. 3 is the ceiling and will * send errors. Defaults to debug message when the level is invalid. * @param message The message to send to logcat. */ public static void sendLog(int level, String message) { String tag = "HgLogger"; switch (level) { default: case LogLevel.DEBUG: Log.d(tag, message); break; case LogLevel.VERBOSE: Log.v(tag, message); break; case LogLevel.WARNING: Log.w(tag, message); break; case LogLevel.ERROR: Log.e(tag, message); break; } } /** * Checks whether the system SDK version is around the specified version. * * @param version Version expected to compare against the system's SDK. * * @return True if system SDK version is equal to or more than the specified version. */ public static boolean sdkIsAround(int version) { return Build.VERSION.SDK_INT >= version; } /** * Checks whether the system SDK version is below a specified version. * * @param version Version expected to compare against the system's SDK. * * @return True if system SDK version is below the specified version. */ public static boolean sdkIsBelow(int version) { return Build.VERSION.SDK_INT < version; } /** * Checks whether the system is at least KitKat. * * @return True when system SDK version is equal to or more than 19 (KitKat). */ public static boolean atLeastKitKat() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; } /** * Checks whether the system is at least Lollipop. * * @return True when the system SDK is equal to or more than 21 (L). */ public static boolean atLeastLollipop() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } /** * Checks whether the system is at least Marshmallow. * * @return True when the system SDK is equal to or more than 23 (M). */ public static boolean atLeastMarshmallow() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } /** * Checks whether the system is at least Oreo. * * @return True when the system SDK is equal to or more than 26 (O). */ public static boolean atLeastOreo() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } /** * Checks whether the system is at least Q. * * @return True when the system SDK is equal to or more than 29 (Q). */ public static boolean atLeastQ() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; } /** * Checks if an object is null; throws an IllegalArgumentException if it is. * * @param obj The object to check for. * @param <T> The type of the referenced object. * * @return Object if not null. */ public static <T> T requireNonNull(T obj) { if (obj == null) { throw new IllegalArgumentException(); } return obj; } /** * Opens a URL/link from a string object. * * @param context Context object for use with startActivity. * @param link The link to be opened. */ public static void openLink(Context context, String link) { Intent linkIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); context.startActivity(linkIntent); } /** * Parses a web provider URL then replaces its placeholder with the query. * * @param context Context object for use with startActivity. * @param provider The URL of the provider * @param query The query itself */ public static void doWebSearch(Context context, String provider, String query) { openLink(context, provider.replace("%s", query)); } /** * Closes a Closeable instance if it is not null. * * @param stream The Closeable instance to close. */ public static void closeStream(Closeable stream) { try { if (stream != null) { stream.close(); } } catch (IOException ignored) { // Do nothing. } } /** * Returns a color from an attribute reference. * * @param context Pass the activity context, not the application context * @param attr The attribute reference to be resolved * * @return int array of color value */ @ColorInt public static int getColorFromAttr(Context context, @AttrRes int attr) { TypedValue typedValue = new TypedValue(); Resources.Theme theme = context.getTheme(); theme.resolveAttribute(attr, typedValue, true); return typedValue.data; } /** * Registers a PackageChangesReceiver on an activity. * * @param activity The activity where PackageChangesReceiver is to be registered. * @param packageReceiver The receiver itself. */ public static void registerPackageReceiver(AppCompatActivity activity, PackageChangesReceiver packageReceiver) { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); intentFilter.addDataScheme("package"); activity.registerReceiver(packageReceiver, intentFilter); } /** * Unregisters a PackageChangesReceiver from an activity. * * @param activity The activity where PackageChangesReceiver is to be registered. * @param packageReceiver The receiver itself. */ public static void unregisterPackageReceiver(AppCompatActivity activity, PackageChangesReceiver packageReceiver) { try { activity.unregisterReceiver(packageReceiver); } catch (IllegalArgumentException w) { Utils.sendLog(LogLevel.DEBUG, "Failed to remove receiver!"); } } /** * Handles gesture actions based on the direction of the gesture. * * @param activity The activity where the gesture is performed. * @param direction The direction of the gesture. * * @see Gesture Valid directions for the gestures. */ public static void handleGestureActions(AppCompatActivity activity, int direction) { UserUtils userUtils = new UserUtils(activity); switch (PreferenceHelper.getGestureForDirection(direction)) { case "handler": if (PreferenceHelper.getGestureHandler() != null) { Intent handlerIntent = new Intent("mono.hg.GESTURE_HANDLER"); handlerIntent.setComponent(PreferenceHelper.getGestureHandler()); handlerIntent.setType("text/plain"); handlerIntent.putExtra("direction", direction); activity.startActivity(handlerIntent); } break; case "widget": WidgetsDialogFragment widgetFragment = new WidgetsDialogFragment(); widgetFragment.show(activity.getSupportFragmentManager(), "Widgets Dialog"); break; case "status": ActivityServiceUtils.expandStatusBar(activity); break; case "panel": ActivityServiceUtils.expandSettingsPanel(activity); break; case "list": // TODO: Maybe make this call less reliant on LauncherActivity? ((LauncherActivity) activity).doThis("show_panel"); break; case "none": // We don't want to do anything. break; default: try { AppUtils.quickLaunch(activity, PreferenceHelper.getGestureForDirection(direction)); } catch (ActivityNotFoundException w) { // Maybe the user had an old configuration, but otherwise, this is harmless. // We should still notify them though. Toast.makeText(activity, activity.getString(R.string.err_activity_null), Toast.LENGTH_LONG).show(); } break; } } /** * Handles common input shortcut from an EditText. * * @param activity The activity to reference for copying and pasting. * @param editText The EditText where the text is being copied/pasted. * @param keyCode Keycode to handle. * * @return True if key is handled. */ public static boolean handleInputShortcut(AppCompatActivity activity, EditText editText, int keyCode) { // Get selected text for cut and copy. int start = editText.getSelectionStart(); int end = editText.getSelectionEnd(); final String text = editText.getText().toString().substring(start, end); switch (keyCode) { case KeyEvent.KEYCODE_A: editText.selectAll(); return true; case KeyEvent.KEYCODE_X: editText.setText(editText.getText().toString().replace(text, "")); return true; case KeyEvent.KEYCODE_C: ActivityServiceUtils.copyToClipboard(activity, text); return true; case KeyEvent.KEYCODE_V: editText.setText( editText.getText().replace(Math.min(start, end), Math.max(start, end), ActivityServiceUtils.pasteFromClipboard(activity), 0, ActivityServiceUtils.pasteFromClipboard(activity).length())); return true; default: // Do nothing. return false; } } /** * Set default providers for indeterminate search. */ public static void setDefaultProviders(Resources resources) { String[] defaultProvider = resources.getStringArray( R.array.pref_search_provider_title); String[] defaultProviderId = resources.getStringArray( R.array.pref_search_provider_values); ArrayList<WebSearchProvider> tempList = new ArrayList<>(); // defaultProvider will always be the same size as defaultProviderUrl. // However, we start at 1 to ignore the 'Always ask' option. for (int i = 1; i < defaultProvider.length; i++) { tempList.add(new WebSearchProvider(defaultProvider[i], PreferenceHelper.getDefaultProvider(defaultProviderId[i]), defaultProvider[i])); } PreferenceHelper.updateProvider(tempList); } /** * The importance (level) of log message. */ @IntDef({LogLevel.DEBUG, LogLevel.VERBOSE, LogLevel.WARNING, LogLevel.ERROR}) @Retention(SOURCE) public @interface LogLevel { int DEBUG = 0; int VERBOSE = 1; int WARNING = 2; int ERROR = 3; } /** * Directions of gesture. */ @IntDef({Gesture.LEFT, Gesture.RIGHT, Gesture.UP, Gesture.DOWN, Gesture.TAP, Gesture.DOUBLE_TAP, Gesture.PINCH}) @Retention(SOURCE) public @interface Gesture { int LEFT = 0; int RIGHT = 1; int UP = 10; int DOWN = 11; int TAP = 100; int DOUBLE_TAP = 101; int PINCH = 111; } }