diff --git a/app/build.gradle b/app/build.gradle index 3d519ffa..c1f0c6df 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,7 @@ android { implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.drawerlayout:drawerlayout:1.1.1" implementation 'androidx.core:core:1.5.0-beta02' + implementation 'com.google.guava:guava:24.1-jre' implementation project(":terminal-view") } diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index c4b98347..e3396ab0 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -51,6 +51,8 @@ import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.app.input.BellHandler; import com.termux.app.input.extrakeys.ExtraKeysView; import com.termux.app.input.FullScreenWorkAround; +import com.termux.app.settings.properties.TermuxPropertyConstants; +import com.termux.app.settings.properties.TermuxSharedProperties; import com.termux.terminal.EmulatorDebug; import com.termux.terminal.TerminalColors; import com.termux.terminal.TerminalSession; @@ -110,6 +112,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection ExtraKeysView mExtraKeysView; TermuxPreferences mSettings; + TermuxSharedProperties mProperties; /** * The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to @@ -144,16 +147,19 @@ public final class TermuxActivity extends Activity implements ServiceConnection public void onReceive(Context context, Intent intent) { if (mIsVisible) { String whatToReload = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE); + Log.d("termux", "Reloading termux style for: " + whatToReload); if ("storage".equals(whatToReload)) { if (ensureStoragePermissionGranted()) TermuxInstaller.setupStorageSymlinks(TermuxActivity.this); return; } + checkForFontAndColors(); - mSettings.reloadFromProperties(TermuxActivity.this); + + mProperties.loadTermuxPropertiesFromDisk(); if (mExtraKeysView != null) { - mExtraKeysView.reload(mSettings.mExtraKeys); + mExtraKeysView.reload(mProperties.getExtraKeysInfo()); } } } @@ -205,7 +211,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection @Override public void onCreate(Bundle bundle) { mSettings = new TermuxPreferences(this); - mIsUsingBlackUI = mSettings.isUsingBlackUI(); + mProperties = new TermuxSharedProperties(this); + + mIsUsingBlackUI = mProperties.isUsingBlackUI(); if (mIsUsingBlackUI) { this.setTheme(R.style.Theme_Termux_Black); } else { @@ -223,7 +231,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection return insets; }); - if (mSettings.isUsingFullScreen()) { + if (mProperties.isUsingFullScreen()) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } @@ -245,7 +253,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams(); - layoutParams.height = layoutParams.height * (mSettings.mExtraKeys == null ? 0 : mSettings.mExtraKeys.getMatrix().length); + layoutParams.height = layoutParams.height * (mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length); viewPager.setLayoutParams(layoutParams); viewPager.setAdapter(new PagerAdapter() { @@ -266,10 +274,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection View layout; if (position == 0) { layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false); - mExtraKeysView.reload(mSettings.mExtraKeys); + mExtraKeysView.reload(mProperties.getExtraKeysInfo()); // apply extra keys fix if enabled in prefs - if (mSettings.isUsingFullScreen() && mSettings.isUsingFullScreenWorkAround()) { + if (mProperties.isUsingFullScreen() && mProperties.isUsingFullScreenWorkAround()) { FullScreenWorkAround.apply(TermuxActivity.this); } @@ -451,14 +459,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection public void onBell(TerminalSession session) { if (!mIsVisible) return; - switch (mSettings.mBellBehaviour) { - case TermuxPreferences.BELL_BEEP: - mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); - break; - case TermuxPreferences.BELL_VIBRATE: + switch (mProperties.getBellBehaviour()) { + case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE: BellHandler.getInstance(TermuxActivity.this).doBell(); break; - case TermuxPreferences.BELL_IGNORE: + case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP: + mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); + break; + case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE: // Ignore the bell character. break; } @@ -665,7 +673,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection String workingDirectory; if (currentSession == null) { - workingDirectory = mSettings.mDefaultWorkingDir; + workingDirectory = mProperties.getDefaultWorkingDirectory(); } else { workingDirectory = currentSession.getCwd(); } diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java index f4187eef..5567439d 100644 --- a/app/src/main/java/com/termux/app/TermuxPreferences.java +++ b/app/src/main/java/com/termux/app/TermuxPreferences.java @@ -2,61 +2,13 @@ package com.termux.app; import android.content.Context; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.preference.PreferenceManager; -import android.util.Log; import android.util.TypedValue; -import android.widget.Toast; + import com.termux.terminal.TerminalSession; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import androidx.annotation.IntDef; - -import static com.termux.terminal.EmulatorDebug.LOG_TAG; final class TermuxPreferences { - @IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE}) - @Retention(RetentionPolicy.SOURCE) - @interface AsciiBellBehaviour { - } - - final static class KeyboardShortcut { - - KeyboardShortcut(int codePoint, int shortcutAction) { - this.codePoint = codePoint; - this.shortcutAction = shortcutAction; - } - - final int codePoint; - final int shortcutAction; - } - - static final int SHORTCUT_ACTION_CREATE_SESSION = 1; - static final int SHORTCUT_ACTION_NEXT_SESSION = 2; - static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3; - static final int SHORTCUT_ACTION_RENAME_SESSION = 4; - - static final int BELL_VIBRATE = 1; - static final int BELL_BEEP = 2; - static final int BELL_IGNORE = 3; - private final int MIN_FONTSIZE; private static final int MAX_FONTSIZE = 256; @@ -65,34 +17,11 @@ final class TermuxPreferences { private static final String CURRENT_SESSION_KEY = "current_session"; private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on"; - private boolean mUseDarkUI; private boolean mScreenAlwaysOn; private int mFontSize; - - private boolean mUseFullScreen; - private boolean mUseFullScreenWorkAround; - - @AsciiBellBehaviour - int mBellBehaviour = BELL_VIBRATE; - - boolean mBackIsEscape; - boolean mDisableVolumeVirtualKeys; boolean mShowExtraKeys; - String mDefaultWorkingDir; - - ExtraKeysInfos mExtraKeys; - - final List shortcuts = new ArrayList<>(); - - /** - * If value is not in the range [min, max], set it to either min or max. - */ - static int clamp(int value, int min, int max) { - return Math.min(Math.max(value, min), max); - } TermuxPreferences(Context context) { - reloadFromProperties(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); @@ -139,18 +68,6 @@ final class TermuxPreferences { return mScreenAlwaysOn; } - boolean isUsingBlackUI() { - return mUseDarkUI; - } - - boolean isUsingFullScreen() { - return mUseFullScreen; - } - - boolean isUsingFullScreenWorkAround() { - return mUseFullScreenWorkAround; - } - void setScreenAlwaysOn(Context context, boolean newValue) { mScreenAlwaysOn = newValue; PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply(); @@ -169,108 +86,11 @@ final class TermuxPreferences { return null; } - void reloadFromProperties(Context context) { - File propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_PATH); - if (!propsFile.exists()) - propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_SECONDARY_PATH); - - Properties props = new Properties(); - try { - if (propsFile.isFile() && propsFile.canRead()) { - try (FileInputStream in = new FileInputStream(propsFile)) { - props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); - } - } - } catch (Exception e) { - Toast.makeText(context, "Could not open properties file termux.properties: " + e.getMessage(), Toast.LENGTH_LONG).show(); - Log.e("termux", "Error loading props", e); - } - - switch (props.getProperty("bell-character", "vibrate")) { - case "beep": - mBellBehaviour = BELL_BEEP; - break; - case "ignore": - mBellBehaviour = BELL_IGNORE; - break; - default: // "vibrate". - mBellBehaviour = BELL_VIBRATE; - break; - } - - switch (props.getProperty("use-black-ui", "").toLowerCase()) { - case "true": - mUseDarkUI = true; - break; - case "false": - mUseDarkUI = false; - break; - default: - int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - mUseDarkUI = nightMode == Configuration.UI_MODE_NIGHT_YES; - } - - mUseFullScreen = "true".equals(props.getProperty("fullscreen", "false").toLowerCase()); - mUseFullScreenWorkAround = "true".equals(props.getProperty("use-fullscreen-workaround", "false").toLowerCase()); - - mDefaultWorkingDir = props.getProperty("default-working-directory", TermuxConstants.HOME_PATH); - File workDir = new File(mDefaultWorkingDir); - if (!workDir.exists() || !workDir.isDirectory()) { - // Fallback to home directory if user configured working directory is not exist - // or is a regular file. - mDefaultWorkingDir = TermuxConstants.HOME_PATH; - } - - String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; - - try { - String extrakeyProp = props.getProperty("extra-keys", defaultExtraKeys); - String extraKeysStyle = props.getProperty("extra-keys-style", "default"); - mExtraKeys = new ExtraKeysInfos(extrakeyProp, extraKeysStyle); - } catch (JSONException e) { - Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show(); - Log.e("termux", "Error loading props", e); - - try { - mExtraKeys = new ExtraKeysInfos(defaultExtraKeys, "default"); - } catch (JSONException e2) { - e2.printStackTrace(); - Toast.makeText(context, "Can't create default extra keys", Toast.LENGTH_LONG).show(); - mExtraKeys = null; - } - } - - mBackIsEscape = "escape".equals(props.getProperty("back-key", "back")); - mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual")); - - shortcuts.clear(); - parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props); - parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props); - parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props); - parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props); - } - - private void parseAction(String name, int shortcutAction, Properties props) { - String value = props.getProperty(name); - if (value == null) return; - String[] parts = value.toLowerCase().trim().split("\\+"); - String input = parts.length == 2 ? parts[1].trim() : null; - if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) { - Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+"); - return; - } - - char c = input.charAt(0); - int codePoint = c; - if (Character.isLowSurrogate(c)) { - if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) { - Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+"); - return; - } else { - codePoint = Character.toCodePoint(input.charAt(1), c); - } - } - shortcuts.add(new KeyboardShortcut(codePoint, shortcutAction)); + /** + * If value is not in the range [min, max], set it to either min or max. + */ + static int clamp(int value, int min, int max) { + return Math.min(Math.max(value, min), max); } } diff --git a/app/src/main/java/com/termux/app/TermuxViewClient.java b/app/src/main/java/com/termux/app/TermuxViewClient.java index 3e92c933..b6eea4d3 100644 --- a/app/src/main/java/com/termux/app/TermuxViewClient.java +++ b/app/src/main/java/com/termux/app/TermuxViewClient.java @@ -8,7 +8,9 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.InputMethodManager; +import com.termux.app.input.KeyboardShortcut; import com.termux.app.input.extrakeys.ExtraKeysView; +import com.termux.app.settings.properties.TermuxPropertyConstants; import com.termux.terminal.KeyHandler; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; @@ -47,7 +49,7 @@ public final class TermuxViewClient implements TerminalViewClient { @Override public boolean shouldBackButtonBeMappedToEscape() { - return mActivity.mSettings.mBackIsEscape; + return mActivity.mProperties.isBackKeyTheEscapeKey(); } @Override @@ -230,23 +232,23 @@ public final class TermuxViewClient implements TerminalViewClient { return true; } - List shortcuts = mActivity.mSettings.shortcuts; + List shortcuts = mActivity.mProperties.getSessionShortcuts(); if (!shortcuts.isEmpty()) { int codePointLowerCase = Character.toLowerCase(codePoint); for (int i = shortcuts.size() - 1; i >= 0; i--) { - TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i); + KeyboardShortcut shortcut = shortcuts.get(i); if (codePointLowerCase == shortcut.codePoint) { switch (shortcut.shortcutAction) { - case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION: + case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION: mActivity.addNewSession(false, null); return true; - case TermuxPreferences.SHORTCUT_ACTION_PREVIOUS_SESSION: - mActivity.switchToSession(false); - return true; - case TermuxPreferences.SHORTCUT_ACTION_NEXT_SESSION: + case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION: mActivity.switchToSession(true); return true; - case TermuxPreferences.SHORTCUT_ACTION_RENAME_SESSION: + case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION: + mActivity.switchToSession(false); + return true; + case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION: mActivity.renameSession(mActivity.getCurrentTermSession()); return true; } @@ -266,7 +268,7 @@ public final class TermuxViewClient implements TerminalViewClient { /** Handle dedicated volume buttons as virtual keys if applicable. */ private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { InputDevice inputDevice = event.getDevice(); - if (mActivity.mSettings.mDisableVolumeVirtualKeys) { + if (mActivity.mProperties.areVirtualVolumeKeysDisabled()) { return false; } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { // Do not steal dedicated buttons from a full external keyboard. diff --git a/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java new file mode 100644 index 00000000..db6eb637 --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java @@ -0,0 +1,312 @@ +package com.termux.app.settings.properties; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import com.google.common.primitives.Primitives; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * An implementation similar to android's {@link android.content.SharedPreferences} interface for + * reading and writing to and from ".properties" files which also maintains an in-memory cache for + * the key/value pairs. Operations are does under synchronization locks and should be thread safe. + * + * Two types of in-memory cache maps are maintained, one for the literal {@link String} values found + * in the file for the keys and an additional one that stores (near) primitive {@link Object} values for + * internal use by the caller. + * + * This currently only has read support, write support can/will be added later if needed. Check android's + * SharedPreferencesImpl class for reference implementation. + * + * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/core/java/android/app/SharedPreferencesImpl.java + */ +public class SharedProperties { + + /** + * The {@link Properties} object that maintains an in-memory cache of values loaded from the + * {@link #mPropertiesFile} file. The key/value pairs are of any keys defined by + * {@link #mPropertiesList} that are found in the file against their literal values in the file. + */ + private Properties mProperties; + + /** + * The {@link HashMap<>} object that maintains an in-memory cache of internal values for the values + * loaded from the {@link #mPropertiesFile} file. The key/value pairs are of any keys defined by + * {@link #mPropertiesList} that are found in the file against their internal {@link Object} values + * returned by the call to + * {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context, String, String)} interface. + */ + private Map mMap; + + private final Context mContext; + private final File mPropertiesFile; + private final Set mPropertiesList; + private final SharedPropertiesParser mSharedPropertiesParser; + + private final Object mLock = new Object(); + + /** + * Constructor for the SharedProperties class. + * + * @param context The Context for operations. + * @param propertiesFile The {@link File} object to load properties from. + * @param propertiesList The {@link Set} object that defined which properties to load. + * @param sharedPropertiesParser that implements the {@link SharedPropertiesParser} interface. + */ + public SharedProperties(@Nonnull Context context, @Nullable File propertiesFile, @Nonnull Set propertiesList, @Nonnull SharedPropertiesParser sharedPropertiesParser) { + mContext = context; + mPropertiesFile = propertiesFile; + mPropertiesList = propertiesList; + mSharedPropertiesParser = sharedPropertiesParser; + + mProperties = new Properties(); + mMap = new HashMap(); + } + + /** + * Load the properties defined by {@link #mPropertiesList} from the {@link #mPropertiesFile} file + * to update the {@link #mProperties} and {@link #mMap} in-memory cache. + * Properties are not loading automatically when constructor is called and must be manually called. + */ + public void loadPropertiesFromDisk() { + synchronized (mLock) { + // Get properties from mPropertiesFile + Properties properties = getProperties(false); + + // We still need to load default values into mMap, so we assume no properties defined if + // reading from mPropertiesFile failed + if (properties == null) + properties = new Properties(); + + HashMap map = new HashMap(); + Properties newProperties = new Properties(); + + String value; + Object internalValue; + for (String key : mPropertiesList) { + value = properties.getProperty(key); // value will be null if key does not exist in propertiesFile + Log.d("termux", key + " : " + value); + + // Call the {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)} + // interface method to get the internal value to store in the {@link #mMap}. + internalValue = mSharedPropertiesParser.getInternalPropertyValueFromValue(mContext, key, value); + + // If the internal value was successfully added to map, then also add value to newProperties + // We only store values in-memory defined by {@link #mPropertiesList} + if (putToMap(map, key, internalValue)) { // null internalValue will be put into map + putToProperties(newProperties, key, value); // null value will **not** be into properties + } + } + + mMap = map; + mProperties = newProperties; + } + } + + /** + * Put a value in a {@link #mMap}. + * The key cannot be {@code null}. + * Only {@code null}, primitive or their wrapper classes or String class objects are allowed to be added to + * the map, although this limitation may be changed. + * + * @param map The {@link Map} object to add value to. + * @param key The key for which to add the value to the map. + * @param value The {@link Object} to add to the map. + * @return Returns {@code true} if value was successfully added, otherwise {@code false}. + */ + public static boolean putToMap(HashMap map, String key, Object value) { + + if (map == null) { + Log.e("termux", "Map passed to SharedProperties.putToProperties() is null"); + return false; + } + + // null keys are not allowed to be stored in mMap + if (key == null) { + Log.e("termux", "Cannot put a null key into properties map"); + return false; + } + + boolean put = false; + if (value != null) { + Class clazz = value.getClass(); + if (clazz.isPrimitive() || Primitives.isWrapperType(clazz) || value instanceof String) { + put = true; + } + } else { + put = true; + } + + if (put) { + map.put(key, value); + return true; + } else { + Log.e("termux", "Cannot put a non-primitive value for the key \"" + key + "\" into properties map"); + return false; + } + } + + /** + * Put a value in a {@link Map}. + * The key cannot be {@code null}. + * Passing {@code null} as the value argument is equivalent to removing the key from the + * properties. + * + * @param properties The {@link Properties} object to add value to. + * @param key The key for which to add the value to the properties. + * @param value The {@link String} to add to the properties. + * @return Returns {@code true} if value was successfully added, otherwise {@code false}. + */ + public static boolean putToProperties(Properties properties, String key, String value) { + + if (properties == null) { + Log.e("termux", "Properties passed to SharedProperties.putToProperties() is null"); + return false; + } + + // null keys are not allowed to be stored in mMap + if (key == null) { + Log.e("termux", "Cannot put a null key into properties"); + return false; + } + + if (value != null) { + properties.put(key, value); + return true; + } else { + properties.remove(key); + } + + return true; + } + + /** + * A static function to get the {@link Properties} object for the propertiesFile. A lock is not + * taken when this function is called. + * + * @param context The {@link Context} to use to show a flash if an exception is raised while + * reading the file. If context is {@code null}, then flash will not be shown. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @return Returns the {@link Properties} object. It will be {@code null} if an exception is + * raised while reading the file. + */ + public static Properties getPropertiesFromFile(Context context, File propertiesFile) { + Properties properties = new Properties(); + + if (propertiesFile == null) { + Log.e("termux", "Not loading properties since file is null"); + return properties; + } + + try { + try (FileInputStream in = new FileInputStream(propertiesFile)) { + Log.v("termux", "Loading properties from \"" + propertiesFile.getAbsolutePath() + "\" file"); + properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + } catch (Exception e) { + if(context != null) + Toast.makeText(context, "Could not open properties file \"" + propertiesFile.getAbsolutePath() + "\": " + e.getMessage(), Toast.LENGTH_LONG).show(); + Log.e("termux", "Error loading properties file \"" + propertiesFile.getAbsolutePath() + "\"", e); + return null; + } + + return properties; + } + + /** + * Get the {@link Properties} object for the {@link #mPropertiesFile}. The {@link Properties} + * object will also contain properties not defined by the {@link #mPropertiesList} if cache + * value is {@code false}. + * + * @param cached If {@code true}, then the {@link #mProperties} in-memory cache is returned. Otherwise + * the {@link Properties} object is directly read from the {@link #mPropertiesFile}. + * @return Returns the {@link Properties} object if read from file, otherwise a copy of {@link #mProperties}. + */ + public Properties getProperties(boolean cached) { + synchronized (mLock) { + if (cached) { + if (mProperties == null) mProperties = new Properties(); + return getPropertiesCopy(mProperties); + } else { + return getPropertiesFromFile(mContext, mPropertiesFile); + } + } + } + + /** + * Get the {@link String} value for the key passed from the {@link #mPropertiesFile}. + * + * @param key The key to read from the {@link Properties} object. + * @param cached If {@code true}, then the value is returned from the {@link #mProperties} in-memory cache. + * Otherwise the {@link Properties} object is read directly from the {@link #mPropertiesFile} + * and value is returned from it against the key. + * @return Returns the {@link String} object. This will be {@code null} if key is not found. + */ + public String getProperty(String key, boolean cached) { + synchronized (mLock) { + return (String) getProperties(cached).get(key); + } + } + + /** + * Get the {@link #mMap} object for the {@link #mPropertiesFile}. A call to + * {@link #loadPropertiesFromDisk()} must be made before this. + * + * @return Returns a copy of {@link #mMap} object. + */ + public Map getInternalProperties() { + synchronized (mLock) { + if (mMap == null) mMap = new HashMap(); + return getMapCopy(mMap); + } + } + + /** + * Get the internal {@link Object} value for the key passed from the {@link #mPropertiesFile}. + * The value is returned from the {@link #mMap} in-memory cache, so a call to + * {@link #loadPropertiesFromDisk()} must be made before this. + * + * @param key The key to read from the {@link #mMap} object. + * @return Returns the {@link Object} object. This will be {@code null} if key is not found or + * if object was {@code null}. Use {@link HashMap#containsKey(Object)} to detect the later. + * situation. + */ + public Object getInternalProperty(String key) { + synchronized (mLock) { + // null keys are not allowed to be stored in mMap + if (key != null) + return getInternalProperties().get(key); + else + return null; + } + } + + public static Properties getPropertiesCopy(Properties inputProperties) { + if(inputProperties == null) return null; + + Properties outputProperties = new Properties(); + for (String key : inputProperties.stringPropertyNames()) { + outputProperties.put(key, inputProperties.get(key)); + } + + return outputProperties; + } + + public static Map getMapCopy(Map map) { + if(map == null) return null; + return new HashMap(map); + } + +} diff --git a/app/src/main/java/com/termux/app/settings/properties/SharedPropertiesParser.java b/app/src/main/java/com/termux/app/settings/properties/SharedPropertiesParser.java new file mode 100644 index 00000000..a82141bf --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/properties/SharedPropertiesParser.java @@ -0,0 +1,23 @@ +package com.termux.app.settings.properties; + +import android.content.Context; + +import java.util.HashMap; + +/** + * An interface that must be defined by the caller of the {@link SharedProperties} class. + */ +public interface SharedPropertiesParser { + + /** + * A function that should return the internal {@link Object} to be stored for a key/value pair + * read from properties file in the {@link HashMap <>} in-memory cache. + * + * @param context The context for operations. + * @param key The key for which the internal object is required. + * @param value The literal value for the property found is the properties file. + * @return Returns the {@link Object} object to store in the {@link HashMap <>} in-memory cache. + */ + public Object getInternalPropertyValueFromValue(Context context, String key, String value); + +} diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java new file mode 100644 index 00000000..21a06ad8 --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java @@ -0,0 +1,228 @@ +package com.termux.app.settings.properties; + +import android.util.Log; + +import com.google.common.collect.ImmutableBiMap; +import com.termux.app.TermuxConstants; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +/* + * Version: v0.1.0 + * + * Changelog + * + * - 0.1.0 (2021-03-08) + * - Initial Release + * + */ + +/** + * A class that defines shared constants of the properties used by Termux app and its plugins. + * This class will be hosted by termux-app and should be imported by other termux plugin apps as is + * instead of copying constants to random classes. The 3rd party apps can also import it for + * interacting with termux apps. If changes are made to this file, increment the version number + * * and add an entry in the Changelog section above. + * + * The properties are loaded from the first file found at + * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_PATH} or + * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_PATH} + */ +public final class TermuxPropertyConstants { + + /** Defines the bidirectional map for boolean values and their internal values */ + public static final ImmutableBiMap MAP_GENERIC_BOOLEAN = + new ImmutableBiMap.Builder() + .put("true", true) + .put("false", false) + .build(); + + /** Defines the bidirectional map for inverted boolean values and their internal values */ + public static final ImmutableBiMap MAP_GENERIC_INVERTED_BOOLEAN = + new ImmutableBiMap.Builder() + .put("true", false) + .put("false", true) + .build(); + + + + /** Defines the key for whether to use back key as the escape key */ + public static final String KEY_USE_BACK_KEY_AS_ESCAPE_KEY = "back-key"; // Default: "back-key" + + public static final String VALUE_BACK_KEY_BEHAVIOUR_BACK = "back"; + public static final String VALUE_BACK_KEY_BEHAVIOUR_ESCAPE = "escape"; + + + + /** Defines the key for whether to use black UI */ + public static final String KEY_USE_BLACK_UI = "use-black-ui"; // Default: "use-black-ui" + + + + /** Defines the key for whether to use fullscreen */ + public static final String KEY_USE_FULLSCREEN = "fullscreen"; // Default: "fullscreen" + + + + /** Defines the key for whether to use fullscreen workaround */ + public static final String KEY_USE_FULLSCREEN_WORKAROUND = "use-fullscreen-workaround"; // Default: "use-fullscreen-workaround" + + + + /** Defines the key for whether virtual volume keys are disabled */ + public static final String KEY_VIRTUAL_VOLUME_KEYS_DISABLED = "volume-keys"; // Default: "volume-keys" + + public static final String VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME = "volume"; + public static final String VALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL = "virtual"; + + + + /** Defines the key for the bell behaviour */ + public static final String KEY_BELL_BEHAVIOUR = "bell-character"; // Default: "bell-character" + + public static final String VALUE_BELL_BEHAVIOUR_VIBRATE = "vibrate"; + public static final String VALUE_BELL_BEHAVIOUR_BEEP = "beep"; + public static final String VALUE_BELL_BEHAVIOUR_IGNORE = "ignore"; + public static final String DEFAULT_VALUE_BELL_BEHAVIOUR = VALUE_BELL_BEHAVIOUR_VIBRATE; + + public static final int IVALUE_BELL_BEHAVIOUR_VIBRATE = 1; + public static final int IVALUE_BELL_BEHAVIOUR_BEEP = 2; + public static final int IVALUE_BELL_BEHAVIOUR_IGNORE = 3; + public static final int DEFAULT_IVALUE_BELL_BEHAVIOUR = IVALUE_BELL_BEHAVIOUR_VIBRATE; + + /** Defines the bidirectional map for bell behaviour values and their internal values */ + public static final ImmutableBiMap MAP_BELL_BEHAVIOUR = + new ImmutableBiMap.Builder() + .put(VALUE_BELL_BEHAVIOUR_VIBRATE, IVALUE_BELL_BEHAVIOUR_VIBRATE) + .put(VALUE_BELL_BEHAVIOUR_BEEP, IVALUE_BELL_BEHAVIOUR_BEEP) + .put(VALUE_BELL_BEHAVIOUR_IGNORE, IVALUE_BELL_BEHAVIOUR_IGNORE) + .build(); + + + + /** Defines the key for create session shortcut */ + public static final String KEY_SHORTCUT_CREATE_SESSION = "shortcut.create-session"; // Default: "shortcut.create-session" + /** Defines the key for next session shortcut */ + public static final String KEY_SHORTCUT_NEXT_SESSION = "shortcut.next-session"; // Default: "shortcut.next-session" + /** Defines the key for previous session shortcut */ + public static final String KEY_SHORTCUT_PREVIOUS_SESSION = "shortcut.previous-session"; // Default: "shortcut.previous-session" + /** Defines the key for rename session shortcut */ + public static final String KEY_SHORTCUT_RENAME_SESSION = "shortcut.rename-session"; // Default: "shortcut.rename-session" + + public static final int ACTION_SHORTCUT_CREATE_SESSION = 1; + public static final int ACTION_SHORTCUT_NEXT_SESSION = 2; + public static final int ACTION_SHORTCUT_PREVIOUS_SESSION = 3; + public static final int ACTION_SHORTCUT_RENAME_SESSION = 4; + + /** Defines the bidirectional map for session shortcut values and their internal actions */ + public static final ImmutableBiMap MAP_SESSION_SHORTCUTS = + new ImmutableBiMap.Builder() + .put(KEY_SHORTCUT_CREATE_SESSION, ACTION_SHORTCUT_CREATE_SESSION) + .put(KEY_SHORTCUT_NEXT_SESSION, ACTION_SHORTCUT_NEXT_SESSION) + .put(KEY_SHORTCUT_PREVIOUS_SESSION, ACTION_SHORTCUT_PREVIOUS_SESSION) + .put(KEY_SHORTCUT_RENAME_SESSION, ACTION_SHORTCUT_RENAME_SESSION) + .build(); + + + + /** Defines the key for the default working directory */ + public static final String KEY_DEFAULT_WORKING_DIRECTORY = "default-working-directory"; // Default: "default-working-directory" + /** Defines the default working directory */ + public static final String DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY = TermuxConstants.HOME_PATH; + + + + /** Defines the key for extra keys */ + public static final String KEY_EXTRA_KEYS = "extra-keys"; // Default: "extra-keys" + /** Defines the key for extra keys style */ + public static final String KEY_EXTRA_KEYS_STYLE = "extra-keys-style"; // Default: "extra-keys-style" + public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; + public static final String DEFAULT_IVALUE_EXTRA_KEYS_STYLE = "default"; + + + + + + /** Defines the set for keys loaded by termux + * Setting this to {@code null} will make {@link SharedProperties} throw an exception. + * */ + public static final Set TERMUX_PROPERTIES_LIST = new HashSet<>(Arrays.asList( + // boolean + KEY_USE_BACK_KEY_AS_ESCAPE_KEY, + KEY_USE_BLACK_UI, + KEY_USE_FULLSCREEN, + KEY_USE_FULLSCREEN_WORKAROUND, + KEY_VIRTUAL_VOLUME_KEYS_DISABLED, + TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, + + // int + KEY_BELL_BEHAVIOUR, + + // Integer + KEY_SHORTCUT_CREATE_SESSION, + KEY_SHORTCUT_NEXT_SESSION, + KEY_SHORTCUT_PREVIOUS_SESSION, + KEY_SHORTCUT_RENAME_SESSION, + + // String + KEY_DEFAULT_WORKING_DIRECTORY, + KEY_EXTRA_KEYS, + KEY_EXTRA_KEYS_STYLE + )); + + /** Defines the set for keys loaded by termux that have default boolean behaviour + * "true" -> true + * "false" -> false + * default: false + * */ + public static final Set TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList( + KEY_USE_FULLSCREEN, + KEY_USE_FULLSCREEN_WORKAROUND, + TermuxConstants.PROP_ALLOW_EXTERNAL_APPS + )); + + /** Defines the set for keys loaded by termux that have default inverted boolean behaviour + * "false" -> true + * "true" -> false + * default: true + * */ + public static final Set TERMUX_DEFAULT_INVERETED_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList( + )); + + + + + /** Returns the first {@link File} found at + * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_PATH} or + * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_PATH} + * from which termux properties can be loaded. + * If the {@link File} found is not a regular file or is not readable then null is returned. + * + * @return Returns the {@link File} object for termux properties. + */ + public static File getTermuxPropertiesFile() { + String[] possiblePropertiesFileLocations = { + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_PATH, + TermuxConstants.TERMUX_PROPERTIES_SECONDARY_PATH + }; + + File propertiesFile = new File(possiblePropertiesFileLocations[0]); + int i = 0; + while (!propertiesFile.exists() && i < possiblePropertiesFileLocations.length) { + propertiesFile = new File(possiblePropertiesFileLocations[i]); + i += 1; + } + + if (propertiesFile.isFile() && propertiesFile.canRead()) { + return propertiesFile; + } else { + Log.d("termux", "No readable termux.properties file found"); + return null; + } + } + +} diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java new file mode 100644 index 00000000..4e2e9c7d --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java @@ -0,0 +1,569 @@ +package com.termux.app.settings.properties; + +import android.content.Context; +import android.content.res.Configuration; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.app.input.extrakeys.ExtraKeysInfo; +import com.termux.app.input.KeyboardShortcut; + +import org.json.JSONException; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.annotation.Nonnull; + +public class TermuxSharedProperties implements SharedPropertiesParser { + + private final Context mContext; + private final SharedProperties mSharedProperties; + private final File mPropertiesFile; + + private ExtraKeysInfo mExtraKeysInfo; + private final List mSessionShortcuts = new ArrayList<>(); + + public TermuxSharedProperties(@Nonnull Context context) { + mContext = context; + mPropertiesFile = TermuxPropertyConstants.getTermuxPropertiesFile(); + mSharedProperties = new SharedProperties(context, mPropertiesFile, TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, this); + loadTermuxPropertiesFromDisk(); + } + + + + + /** + * Reload the termux properties from disk into an in-memory cache. + */ + public void loadTermuxPropertiesFromDisk() { + mSharedProperties.loadPropertiesFromDisk(); + dumpPropertiesToLog(); + dumpInternalPropertiesToLog(); + setExtraKeys(); + setSessionShortcuts(); + } + + /** + * Set the terminal extra keys and style. + */ + private void setExtraKeys() { + mExtraKeysInfo = null; + + try { + // The mMap stores the extra key and style string values while loading properties + // Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and + // {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)} + String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true); + String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true); + mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle); + } catch (JSONException e) { + Toast.makeText(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), Toast.LENGTH_LONG).show(); + Log.e("termux", "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e); + + try { + mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE); + } catch (JSONException e2) { + Toast.makeText(mContext, "Can't create default extra keys", Toast.LENGTH_LONG).show(); + Log.e("termux", "Could create default extra keys: ", e); + mExtraKeysInfo = null; + } + } + } + + /** + * Set the terminal sessions shortcuts. + */ + private void setSessionShortcuts() { + mSessionShortcuts.clear(); + // The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair + for (Map.Entry entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) { + // The mMap stores the code points for the session shortcuts while loading properties + Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true); + // If codePoint is null, then session shortcut did not exist in properties or was invalid + // as parsed by {@link #getCodePointForSessionShortcuts(String,String)} + // If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and + // add the code point to sessionShortcuts + if (codePoint != null) + mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue())); + } + } + + + + + + /** + * A static function to get the {@link Properties} from the propertiesFile file. + * + * @param context The {@link Context} for the {@link SharedProperties#getPropertiesFromFile(Context,File)} call. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @return Returns the {@link Properties} object. It will be {@code null} if an exception is + * raised while reading the file. + */ + public static Properties getPropertiesFromFile(Context context, File propertiesFile) { + return SharedProperties.getPropertiesFromFile(context, propertiesFile); + } + + /** + * A static function to get the {@link String} value for the {@link Properties} key read from + * the propertiesFile file. + * + * @param context The {@link Context} for the {@link SharedProperties#getPropertiesFromFile(Context,File)} call. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @param key The key to read. + * @param def The default value. + * @return Returns the {@link String} object. This will be {@code null} if key is not found. + */ + public static String getPropertyValue(Context context, File propertiesFile, String key, String def) { + return (String) getDefaultIfNull(getDefaultIfNull(getPropertiesFromFile(context, propertiesFile), new Properties()).get(key), def); + } + + /** + * A static function to check if the value is {@code true} for {@link Properties} key read from + * the propertiesFile file. + * + * @param context The {@link Context} for the {@link SharedProperties#getPropertiesFromFile(Context,File)}call. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @param key The key to read. + * @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "true", + * regardless of case. If the key does not exist in the file or does not equal "true", then + * {@code false} will be returned. + */ + public static boolean isPropertyValueTrue(Context context, File propertiesFile, String key) { + return (boolean) getBooleanValueForStringValue((String) getPropertyValue(context, propertiesFile, key, null), false); + } + + /** + * A static function to check if the value is {@code false} for {@link Properties} key read from + * the propertiesFile file. + * + * @param context The {@link Context} for the {@link SharedProperties#getPropertiesFromFile(Context,File)} call. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @param key The key to read. + * @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "false", + * regardless of case. If the key does not exist in the file or does not equal "false", then + * {@code true} will be returned. + */ + public static boolean isPropertyValueFalse(Context context, File propertiesFile, String key) { + return (boolean) getInvertedBooleanValueForStringValue((String) getPropertyValue(context, propertiesFile, key, null), true); + } + + + + + + /** + * Get the {@link Properties} from the {@link #mPropertiesFile} file. + * + * @param cached If {@code true}, then the {@link Properties} in-memory cache is returned. + * Otherwise the {@link Properties} object is read directly from the + * {@link #mPropertiesFile} file. + * @return Returns the {@link Properties} object. It will be {@code null} if an exception is + * raised while reading the file. + */ + public Properties getProperties(boolean cached) { + return mSharedProperties.getProperties(cached); + } + + /** + * Get the {@link String} value for the key passed from the {@link #mPropertiesFile} file. + * + * @param key The key to read. + * @param def The default value. + * @param cached If {@code true}, then the value is returned from the the {@link Properties} in-memory cache. + * Otherwise the {@link Properties} object is read directly from the file + * and value is returned from it against the key. + * @return Returns the {@link String} object. This will be {@code null} if key is not found. + */ + public String getPropertyValue(String key, String def, boolean cached) { + return getDefaultIfNull(mSharedProperties.getProperty(key, cached), def); + } + + /** + * A function to check if the value is {@code true} for {@link Properties} key read from + * the {@link #mPropertiesFile} file. + * + * @param key The key to read. + * @param cached If {@code true}, then the value is checked from the the {@link Properties} in-memory cache. + * Otherwise the {@link Properties} object is read directly from the file + * and value is checked from it. + * @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "true", + * regardless of case. If the key does not exist in the file or does not equal "true", then + * {@code false} will be returned. + */ + public boolean isPropertyValueTrue(String key, boolean cached) { + return (boolean) getBooleanValueForStringValue((String) getPropertyValue(key, null, cached), false); + } + + /** + * A function to check if the value is {@code false} for {@link Properties} key read from + * the {@link #mPropertiesFile} file. + * + * @param key The key to read. + * @param cached If {@code true}, then the value is checked from the the {@link Properties} in-memory cache. + * Otherwise the {@link Properties} object is read directly from the file + * and value is checked from it. + * @return Returns {@code true} if the {@link Properties} key {@link String} value equals "false", + * regardless of case. If the key does not exist in the file or does not equal "false", then + * {@code true} will be returned. + */ + public boolean isPropertyValueFalse(String key, boolean cached) { + return (boolean) getInvertedBooleanValueForStringValue((String) getPropertyValue(key, null, cached), true); + } + + + + + + /** + * Get the internal value {@link Object} {@link HashMap <>} in-memory cache for the + * {@link #mPropertiesFile} file. A call to {@link #loadTermuxPropertiesFromDisk()} must be made + * before this. + * + * @return Returns a copy of {@link Map} object. + */ + public Map getInternalProperties() { + return mSharedProperties.getInternalProperties(); + } + + /** + * Get the internal {@link Object} value for the key passed from the {@link #mPropertiesFile} file. + * If cache is {@code true}, then value is returned from the {@link HashMap <>} in-memory cache, + * so a call to {@link #loadTermuxPropertiesFromDisk()} must be made before this. + * + * @param key The key to read from the {@link HashMap<>} in-memory cache. + * @param cached If {@code true}, then the value is returned from the the {@link HashMap <>} in-memory cache, + * but if the value is null, then an attempt is made to return the default value. + * If {@code false}, then the {@link Properties} object is read directly from the file + * and internal value is returned for the property value against the key. + * @return Returns the {@link Object} object. This will be {@code null} if key is not found or + * the object stored against the key is {@code null}. + */ + public Object getInternalPropertyValue(String key, boolean cached) { + Object value; + if (cached) { + value = mSharedProperties.getInternalProperty(key); + // If the value is not null since key was found or if the value was null since the + // object stored for the key was itself null, we detect the later by checking if the key + // exists in the map. + if (value != null || mSharedProperties.getInternalProperties().containsKey(key)) { + return value; + } else { + // This should not happen normally unless mMap was modified after the + // {@link #loadTermuxPropertiesFromDisk()} call + // A null value can still be returned by + // {@link #getInternalPropertyValueFromValue(Context,String,String)} for some keys + value = getInternalPropertyValueFromValue(mContext, key, null); + Log.w("termux", "The value for \"" + key + "\" not found in SharedProperties cahce, force returning default value: `" + value + "`"); + return value; + } + } else { + // We get the property value directly from file and return its internal value + return getInternalPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false)); + } + } + + /** + * Override the + * {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)} + * interface function. + */ + @Override + public Object getInternalPropertyValueFromValue(Context context, String key, String value) { + return getInternalTermuxPropertyValueFromValue(context, key, value); + } + + /** + * A static function that should return the internal termux {@link Object} for a key/value pair + * read from properties file. + * + * @param context The context for operations. + * @param key The key for which the internal object is required. + * @param value The literal value for the property found is the properties file. + * @return Returns the internal termux {@link Object} object. + */ + public static Object getInternalTermuxPropertyValueFromValue(Context context, String key, String value) { + if(key == null) return null; + /* + For keys where a MAP_* is checked by respective functions. Note that value to this function + would actually be the key for the MAP_*: + - If the value is currently null, then searching MAP_* should also return null and internal default value will be used. + - If the value is not null and does not exist in MAP_*, then internal default value will be used. + - If the value is not null and does exist in MAP_*, then internal value returned by map will be used. + */ + switch (key) { + // boolean + case TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY: + return (boolean) getUseBackKeyAsEscapeKeyInternalPropertyValueFromValue(value); + case TermuxPropertyConstants.KEY_USE_BLACK_UI: + return (boolean) getUseBlackUIInternalPropertyValueFromValue(context, value); + case TermuxPropertyConstants.KEY_VIRTUAL_VOLUME_KEYS_DISABLED: + return (boolean) getVolumeKeysDisabledInternalPropertyValueFromValue(value); + + // int + case TermuxPropertyConstants.KEY_BELL_BEHAVIOUR: + return (int) getBellBehaviourInternalPropertyValueFromValue(value); + + // Integer (may be null) + case TermuxPropertyConstants.KEY_SHORTCUT_CREATE_SESSION: + case TermuxPropertyConstants.KEY_SHORTCUT_NEXT_SESSION: + case TermuxPropertyConstants.KEY_SHORTCUT_PREVIOUS_SESSION: + case TermuxPropertyConstants.KEY_SHORTCUT_RENAME_SESSION: + return (Integer) getCodePointForSessionShortcuts(key, value); + + // String (may be null) + case TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY: + return (String) getDefaultWorkingDirectoryInternalPropertyValueFromValue(value); + case TermuxPropertyConstants.KEY_EXTRA_KEYS: + return (String) getExtraKeysInternalPropertyValueFromValue(value); + case TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE: + return (String) getExtraKeysStyleInternalPropertyValueFromValue(value); + default: + // default boolean behaviour + if(TermuxPropertyConstants.TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key)) + return (boolean) getBooleanValueForStringValue(value, false); + // default inverted boolean behaviour + else if(TermuxPropertyConstants.TERMUX_DEFAULT_INVERETED_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key)) + return (boolean) getInvertedBooleanValueForStringValue(value, true); + // just use String object as is (may be null) + else + return value; + } + } + + + + + + /** + * Get the boolean value for the {@link String} value. + * + * @param value The {@link String} value to convert. + * @param def The default {@link boolean} value to return. + * @return Returns {@code true} or {@code false} if value is the literal string "true" or "false" respectively, + * regardless of case. Otherwise returns default value. + */ + public static boolean getBooleanValueForStringValue(String value, boolean def) { + return (boolean) getDefaultIfNull(TermuxPropertyConstants.MAP_GENERIC_BOOLEAN.get(toLowerCase(value)), def); + } + + /** + * Get the inverted boolean value for the {@link String} value. + * + * @param value The {@link String} value to convert. + * @param def The default {@link boolean} value to return. + * @return Returns {@code true} or {@code false} if value is the literal string "false" or "true" respectively, + * regardless of case. Otherwise returns default value. + */ + public static boolean getInvertedBooleanValueForStringValue(String value, boolean def) { + return (boolean) getDefaultIfNull(TermuxPropertyConstants.MAP_GENERIC_INVERTED_BOOLEAN.get(toLowerCase(value)), def); + } + + /** + * Returns {@code true} if value is not {@code null} and equals {@link TermuxPropertyConstants#VALUE_BACK_KEY_BEHAVIOUR_ESCAPE}, otherwise false. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static boolean getUseBackKeyAsEscapeKeyInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(value, TermuxPropertyConstants.VALUE_BACK_KEY_BEHAVIOUR_BACK).equals(TermuxPropertyConstants.VALUE_BACK_KEY_BEHAVIOUR_ESCAPE); + } + + /** + * Returns {@code true} or {@code false} if value is the literal string "true" or "false" respectively regardless of case. + * Otherwise returns {@code true} if the night mode is currently enabled in the system. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static boolean getUseBlackUIInternalPropertyValueFromValue(Context context, String value) { + int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + return getBooleanValueForStringValue(value, nightMode == Configuration.UI_MODE_NIGHT_YES); + } + + /** + * Returns {@code true} if value is not {@code null} and equals {@link TermuxPropertyConstants#VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME}, otherwise {@code false}. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static boolean getVolumeKeysDisabledInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(value, TermuxPropertyConstants.VALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL).equals(TermuxPropertyConstants.VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME); + } + + /** + * Returns {@code true} if value is not {@code null} and equals {@link TermuxPropertyConstants#VALUE_BACK_KEY_BEHAVIOUR_ESCAPE}, otherwise {@code false}. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static int getBellBehaviourInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(TermuxPropertyConstants.MAP_BELL_BEHAVIOUR.get(toLowerCase(value)), TermuxPropertyConstants.DEFAULT_IVALUE_BELL_BEHAVIOUR); + } + + /** + * Returns the code point for the value if key is not {@code null} and value is not {@code null} and is valid, + * otherwise returns {@code null}. + * + * @param key The key for session shortcut. + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static Integer getCodePointForSessionShortcuts(String key, String value) { + if (key == null) return null; + if (value == null) return null; + String[] parts = value.toLowerCase().trim().split("\\+"); + String input = parts.length == 2 ? parts[1].trim() : null; + if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) { + Log.e("termux", "Keyboard shortcut '" + key + "' is not Ctrl+"); + return null; + } + + char c = input.charAt(0); + int codePoint = c; + if (Character.isLowSurrogate(c)) { + if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) { + Log.e("termux", "Keyboard shortcut '" + key + "' is not Ctrl+"); + return null; + } else { + codePoint = Character.toCodePoint(input.charAt(1), c); + } + } + + return codePoint; + } + + /** + * Returns the path itself if a directory exists at it and is readable, otherwise returns + * {@link TermuxPropertyConstants#DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY}. + * + * @param path The {@link String} path to check. + * @return Returns the internal value for value. + */ + public static String getDefaultWorkingDirectoryInternalPropertyValueFromValue(String path) { + if (path == null || path.isEmpty()) return TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY; + File workDir = new File(path); + if (!workDir.exists() || !workDir.isDirectory() || !workDir.canRead()) { + // Fallback to default directory if user configured working directory does not exist + // or is not a directory or is not readable. + return TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY; + } else { + return path; + } + } + + /** + * Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_EXTRA_KEYS}. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static String getExtraKeysInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS); + } + + /** + * Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_EXTRA_KEYS_STYLE}. + * + * @param value {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static String getExtraKeysStyleInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE); + } + + + + + + public boolean isBackKeyTheEscapeKey() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY, true); + } + + public boolean isUsingBlackUI() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BLACK_UI, true); + } + + public boolean isUsingFullScreen() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_FULLSCREEN, true); + } + + public boolean isUsingFullScreenWorkAround() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_FULLSCREEN_WORKAROUND, true); + } + + public boolean areVirtualVolumeKeysDisabled() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_VIRTUAL_VOLUME_KEYS_DISABLED, true); + } + + public int getBellBehaviour() { + return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_BELL_BEHAVIOUR, true); + } + + public List getSessionShortcuts() { + return mSessionShortcuts; + } + + public String getDefaultWorkingDirectory() { + return (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY, true); + } + + public ExtraKeysInfo getExtraKeysInfo() { + return mExtraKeysInfo; + } + + + + + + public static T getDefaultIfNull(@Nullable T object, @Nullable T def) { + return (object == null) ? def : object; + } + + private static String toLowerCase(String value) { + if (value == null) return null; else return value.toLowerCase(); + } + + + + + public void dumpPropertiesToLog() { + Properties properties = getProperties(true); + StringBuilder propertiesDump = new StringBuilder(); + + propertiesDump.append("Termux Properties:"); + if (properties != null) { + for (String key : properties.stringPropertyNames()) { + propertiesDump.append("\n").append(key).append(": `").append(properties.get(key)).append("`"); + } + } else { + propertiesDump.append(" null"); + } + + Log.d("termux", propertiesDump.toString()); + } + + public void dumpInternalPropertiesToLog() { + HashMap internalProperties = (HashMap) getInternalProperties(); + StringBuilder internalPropertiesDump = new StringBuilder(); + + internalPropertiesDump.append("Termux Internal Properties:"); + if (internalProperties != null) { + for (String key : internalProperties.keySet()) { + internalPropertiesDump.append("\n").append(key).append(": `").append(internalProperties.get(key)).append("`"); + } + } + + Log.d("termux", internalPropertiesDump.toString()); + } + +}