paulscode.android.mupen64plusae.persistent.UserPrefs.java Source code

Java tutorial

Introduction

Here is the source code for paulscode.android.mupen64plusae.persistent.UserPrefs.java

Source

/**
 * Mupen64PlusAE, an N64 emulator for the Android platform
 * 
 * Copyright (C) 2013 Paul Lamb
 * 
 * This file is part of Mupen64PlusAE.
 * 
 * Mupen64PlusAE 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.
 * 
 * Mupen64PlusAE 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 Mupen64PlusAE. If
 * not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors: littleguy77
 */
package paulscode.android.mupen64plusae.persistent;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.WordUtils;
import org.mupen64plusae.v3.alpha.R;

import paulscode.android.mupen64plusae.jni.NativeConstants;
import paulscode.android.mupen64plusae.persistent.ConfigFile.ConfigSection;
import paulscode.android.mupen64plusae.profile.Profile;
import paulscode.android.mupen64plusae.util.Plugin;
import paulscode.android.mupen64plusae.util.SafeMethods;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.WindowManager;

/**
 * A convenience class for quickly, safely, and consistently retrieving typed user preferences.
 * <p>
 * <b>Developers:</b> After creating a preference in /res/xml/preferences.xml, you are encouraged to
 * provide convenient access to it by expanding this class. Although this adds an extra step to
 * development, it simplifies code maintenance later since all maintenance can be consolidated to a
 * single file. For example, if you change the name of a key, you only need to update one line in
 * this class:
 * 
 * <pre>
 * {@code
 * myPreference = mPreferences.getString( "myOldKey", "myFallbackValue" );
 *            --> mPreferences.getString( "myNewKey", "myFallbackValue" );
 * }
 * </pre>
 * 
 * Without this class, you would need to search through the entire code base for every call to
 * getString( "myOldKey", ... ) and update each one. This class also ensures that the same fallback
 * value will be used everywhere. A third advantage is that you can easily provide frequently-used
 * "derived" preferences, as in
 * 
 * <pre>
 * {@code
 * isMyPreferenceValid = ( myPreference != null ) && ( myPreference.field != someBadValue );
 * }
 * </pre>
 * 
 * Finally, the cost of looking up a preference value is made up front in this class's constructor,
 * rather than at the point of use. This could improve application performance if the value is used
 * often, such as the frame refresh loop of a game.
 */
public class UserPrefs {
    /** The parent directory containing all user-writable data files. */
    public final String userDataDir;

    /** The subdirectory containing gallery data. */
    public final String galleryDataDir;

    /** The subdirectory containing custom profiles. */
    public final String profilesDir;

    /** The subdirectory containing crash logs. */
    public final String crashLogDir;

    /** The subdirectory returned from the core's ConfigGetUserDataPath() method. */
    public final String coreUserDataDir;

    /** The subdirectory returned from the core's ConfigGetUserCachePath() method. */
    public final String coreUserCacheDir;

    /** The subdirectory where hi-res textures must be unzipped. */
    public final String hiResTextureDir;

    /** The path of the rom info cache for the gallery. */
    public final String romInfoCache_cfg;

    /** The path of the custom controller profiles file. */
    public final String controllerProfiles_cfg;

    /** The path of the custom touchscreen profiles file. */
    public final String touchscreenProfiles_cfg;

    /** The path of the custom emulation profiles file. */
    public final String emulationProfiles_cfg;

    /** The path of the user's custom cheat files. */
    public final String customCheats_txt;

    /** The selected audio plug-in. */
    public final Plugin audioPlugin;

    /** True if the touchscreen feedback is enabled. */
    public final boolean isTouchscreenFeedbackEnabled;

    /** The number of frames over which touchscreen is redrawn (0 = disabled). */
    public final int touchscreenRefresh;

    /** The directory of the selected touchscreen skin. */
    public final String touchscreenSkin;

    /** The touchscreen transparency value. */
    public final int touchscreenTransparency;

    /** Factor applied to the final calculated visible touchmap scale. */
    public final float touchscreenScale;

    /** The method used for auto holding buttons. */
    public final int touchscreenAutoHold;

    /** True if Xperia Play touchpad is enabled. */
    public final boolean isTouchpadEnabled;

    /** True if Xperia Play touchpad feedback is enabled. */
    public final boolean isTouchpadFeedbackEnabled;

    /** The directory of the selected Xperia Play skin. */
    public final String touchpadSkin;

    /** The touchpad profile. */
    public final Profile touchpadProfile;

    /** True if a single peripheral device can control multiple players concurrently. */
    public final boolean isControllerShared;

    /** The set of key codes that are not allowed to be mapped. **/
    public final List<Integer> unmappableKeyCodes;

    /** The screen orientation for the game activity. */
    public final int displayOrientation;

    /** The vertical screen position. */
    public final int displayPosition;

    /** The width of the OpenGL rendering context, in pixels. */
    public final int videoRenderWidth;

    /** The height of the OpenGL rendering context, in pixels. */
    public final int videoRenderHeight;

    /** The width of the viewing surface, in pixels. */
    public final int videoSurfaceWidth;

    /** The height of the viewing surface, in pixels. */
    public final int videoSurfaceHeight;

    /** The action bar transparency value. */
    public final int displayActionBarTransparency;

    /** The number of frames over which FPS is calculated (0 = disabled). */
    public final int displayFpsRefresh;

    /** True if the FPS indicator is displayed. */
    public final boolean isFpsEnabled;

    /** True if immersive mode should be used (KitKat only). */
    public final boolean isImmersiveModeEnabled;

    /** True if framelimiter is used. */
    public final boolean isFramelimiterEnabled;

    /** The manually-overridden hardware type, used for flicker reduction. */
    public final int videoHardwareType;

    /** The polygon offset to use if hardware type is 'custom'. */
    public final float videoPolygonOffset;

    /** True if the left and right audio channels are swapped. */
    public final boolean audioSwapChannels;

    /** Size of secondary buffer in output samples. This is SDL's hardware buffer, which directly affects latency. */
    public final int audioSecondaryBufferSize;

    /** True if big-screen navigation mode is enabled. */
    public final boolean isBigScreenMode;

    /** True if the action bar is available. */
    public final boolean isActionBarAvailable;

    // Shared preferences keys and key templates
    private static final String KEY_EMULATION_PROFILE_DEFAULT = "emulationProfileDefault";
    private static final String KEY_TOUCHSCREEN_PROFILE_DEFAULT = "touchscreenProfileDefault";
    private static final String KEY_CONTROLLER_PROFILE_DEFAULT = "controllerProfileDefault";
    private static final String KEYTEMPLATE_PAK_TYPE = "inputPakType%1$d";
    private static final String KEY_PLAYER_MAP_REMINDER = "playerMapReminder";
    private static final String KEY_LOCALE_OVERRIDE = "localeOverride";
    // ... add more as needed

    // Shared preferences default values
    public static final String DEFAULT_EMULATION_PROFILE_DEFAULT = "Balanced-glide64";
    public static final String DEFAULT_TOUCHSCREEN_PROFILE_DEFAULT = AppData.IS_OUYA_HARDWARE ? "" : "Analog";
    public static final String DEFAULT_CONTROLLER_PROFILE_DEFAULT = AppData.IS_OUYA_HARDWARE ? "OUYA" : "";
    public static final int DEFAULT_PAK_TYPE = NativeConstants.PAK_TYPE_MEMORY;
    public static final boolean DEFAULT_PLAYER_MAP_REMINDER = true;
    public static final String DEFAULT_LOCALE_OVERRIDE = "";
    public static final String DEFAULT_PATH_SELECTED_GAME = "~roms/n64";
    // ... add more as needed

    private final SharedPreferences mPreferences;
    private final Locale mLocale;
    private final String mLocaleCode;
    private final String[] mLocaleNames;
    private final String[] mLocaleCodes;

    /**
     * Instantiates a new user preferences wrapper.
     * 
     * @param context The application context.
     */
    @SuppressWarnings("deprecation")
    @SuppressLint("InlinedApi")
    @TargetApi(17)
    public UserPrefs(Context context) {
        AppData appData = new AppData(context);
        mPreferences = PreferenceManager.getDefaultSharedPreferences(context);

        // Locale
        mLocaleCode = mPreferences.getString(KEY_LOCALE_OVERRIDE, DEFAULT_LOCALE_OVERRIDE);
        mLocale = TextUtils.isEmpty(mLocaleCode) ? Locale.getDefault() : createLocale(mLocaleCode);
        Locale[] availableLocales = Locale.getAvailableLocales();
        String[] values = context.getResources().getStringArray(R.array.localeOverride_values);
        String[] entries = new String[values.length];
        for (int i = values.length - 1; i > 0; i--) {
            Locale locale = createLocale(values[i]);

            // Get intersection of languages (available on device) and (translated for Mupen)
            if (ArrayUtils.contains(availableLocales, locale)) {
                // Get the name of the language, as written natively
                entries[i] = WordUtils.capitalize(locale.getDisplayName(locale));
            } else {
                // Remove the item from the list
                entries = (String[]) ArrayUtils.remove(entries, i);
                values = (String[]) ArrayUtils.remove(values, i);
            }
        }
        entries[0] = context.getString(R.string.localeOverride_entrySystemDefault);
        mLocaleNames = entries;
        mLocaleCodes = values;

        // Files
        userDataDir = mPreferences.getString("pathGameSaves", "");
        galleryDataDir = userDataDir + "/GalleryData";
        profilesDir = userDataDir + "/Profiles";
        crashLogDir = userDataDir + "/CrashLogs";
        coreUserDataDir = userDataDir + "/CoreConfig/UserData";
        coreUserCacheDir = userDataDir + "/CoreConfig/UserCache";
        hiResTextureDir = coreUserDataDir + "/mupen64plus/hires_texture/"; // MUST match what rice assumes natively
        romInfoCache_cfg = galleryDataDir + "/romInfoCache.cfg";
        controllerProfiles_cfg = profilesDir + "/controller.cfg";
        touchscreenProfiles_cfg = profilesDir + "/touchscreen.cfg";
        emulationProfiles_cfg = profilesDir + "/emulation.cfg";
        customCheats_txt = profilesDir + "/customCheats.txt";

        // Plug-ins
        audioPlugin = new Plugin(mPreferences, appData.libsDir, "audioPlugin");

        // Touchscreen prefs
        isTouchscreenFeedbackEnabled = mPreferences.getBoolean("touchscreenFeedback", false);
        touchscreenRefresh = getSafeInt(mPreferences, "touchscreenRefresh", 0);
        touchscreenScale = ((float) mPreferences.getInt("touchscreenScale", 100)) / 100.0f;
        touchscreenTransparency = (255 * mPreferences.getInt("touchscreenTransparency", 100)) / 100;
        touchscreenSkin = appData.touchscreenSkinsDir + "/" + mPreferences.getString("touchscreenStyle", "Outline");
        touchscreenAutoHold = getSafeInt(mPreferences, "touchscreenAutoHold", 0);

        // Xperia PLAY touchpad prefs
        isTouchpadEnabled = appData.hardwareInfo.isXperiaPlay && mPreferences.getBoolean("touchpadEnabled", true);
        isTouchpadFeedbackEnabled = mPreferences.getBoolean("touchpadFeedback", false);
        touchpadSkin = appData.touchpadSkinsDir + "/Xperia-Play";
        ConfigFile touchpad_cfg = new ConfigFile(appData.touchpadProfiles_cfg);
        ConfigSection section = touchpad_cfg.get(mPreferences.getString("touchpadLayout", ""));
        if (section != null)
            touchpadProfile = new Profile(true, section);
        else
            touchpadProfile = null;

        // Video prefs
        displayOrientation = getSafeInt(mPreferences, "displayOrientation", 0);
        displayPosition = getSafeInt(mPreferences, "displayPosition", Gravity.CENTER_VERTICAL);
        int transparencyPercent = mPreferences.getInt("displayActionBarTransparency", 50);
        displayActionBarTransparency = (255 * transparencyPercent) / 100;
        displayFpsRefresh = getSafeInt(mPreferences, "displayFpsRefresh", 0);
        isFpsEnabled = displayFpsRefresh > 0;
        videoHardwareType = getSafeInt(mPreferences, "videoHardwareType", -1);
        videoPolygonOffset = SafeMethods.toFloat(mPreferences.getString("videoPolygonOffset", "-0.2"), -0.2f);
        isImmersiveModeEnabled = mPreferences.getBoolean("displayImmersiveMode", false);

        // Audio prefs
        audioSwapChannels = mPreferences.getBoolean("audioSwapChannels", false);
        audioSecondaryBufferSize = getSafeInt(mPreferences, "audioBufferSize", 2048);
        if (audioPlugin.enabled)
            isFramelimiterEnabled = mPreferences.getBoolean("audioSynchronize", true);
        else
            isFramelimiterEnabled = !mPreferences.getString("audioPlugin", "").equals("nospeedlimit");

        // User interface modes
        String navMode = mPreferences.getString("navigationMode", "auto");
        if (navMode.equals("bigscreen"))
            isBigScreenMode = true;
        else if (navMode.equals("standard"))
            isBigScreenMode = false;
        else
            isBigScreenMode = AppData.IS_OUYA_HARDWARE; // TODO: Add other systems as they enter market
        isActionBarAvailable = AppData.IS_HONEYCOMB && !isBigScreenMode;

        // Peripheral share mode
        isControllerShared = mPreferences.getBoolean("inputShareController", false);

        // Determine the key codes that should not be mapped to controls
        boolean volKeysMappable = mPreferences.getBoolean("inputVolumeMappable", false);
        List<Integer> unmappables = new ArrayList<Integer>();
        unmappables.add(KeyEvent.KEYCODE_MENU);
        if (AppData.IS_HONEYCOMB) {
            // Back key is needed to show/hide the action bar in HC+
            unmappables.add(KeyEvent.KEYCODE_BACK);
        }
        if (!volKeysMappable) {
            unmappables.add(KeyEvent.KEYCODE_VOLUME_UP);
            unmappables.add(KeyEvent.KEYCODE_VOLUME_DOWN);
            unmappables.add(KeyEvent.KEYCODE_VOLUME_MUTE);
        }
        unmappableKeyCodes = Collections.unmodifiableList(unmappables);

        // Determine the pixel dimensions of the rendering context and view surface
        {
            // Screen size
            final WindowManager windowManager = (WindowManager) context
                    .getSystemService(android.content.Context.WINDOW_SERVICE);
            Display display = windowManager.getDefaultDisplay();
            int stretchWidth;
            int stretchHeight;
            if (display == null) {
                stretchWidth = stretchHeight = 0;
            } else if (AppData.IS_KITKAT && isImmersiveModeEnabled) {
                DisplayMetrics metrics = new DisplayMetrics();
                display.getRealMetrics(metrics);
                stretchWidth = metrics.widthPixels;
                stretchHeight = metrics.heightPixels;
            } else {
                stretchWidth = display.getWidth();
                stretchHeight = display.getHeight();
            }

            float aspect = 0.75f; // TODO: Handle PAL
            boolean isLetterboxed = ((float) stretchHeight / (float) stretchWidth) > aspect;
            int zoomWidth = isLetterboxed ? stretchWidth : Math.round((float) stretchHeight / aspect);
            int zoomHeight = isLetterboxed ? Math.round((float) stretchWidth * aspect) : stretchHeight;
            int cropWidth = isLetterboxed ? Math.round((float) stretchHeight / aspect) : stretchWidth;
            int cropHeight = isLetterboxed ? stretchHeight : Math.round((float) stretchWidth * aspect);

            int hResolution = getSafeInt(mPreferences, "displayResolution", 0);
            String scaling = mPreferences.getString("displayScaling", "zoom");
            if (hResolution == 0) {
                // Native resolution
                if (scaling.equals("stretch")) {
                    videoRenderWidth = videoSurfaceWidth = stretchWidth;
                    videoRenderHeight = videoSurfaceHeight = stretchHeight;
                } else if (scaling.equals("crop")) {
                    videoRenderWidth = videoSurfaceWidth = cropWidth;
                    videoRenderHeight = videoSurfaceHeight = cropHeight;
                } else // scaling.equals( "zoom") || scaling.equals( "none" )
                {
                    videoRenderWidth = videoSurfaceWidth = zoomWidth;
                    videoRenderHeight = videoSurfaceHeight = zoomHeight;
                }
            } else {
                // Non-native resolution
                switch (hResolution) {
                case 720:
                    videoRenderWidth = 960;
                    videoRenderHeight = 720;
                    break;
                case 600:
                    videoRenderWidth = 800;
                    videoRenderHeight = 600;
                    break;
                case 480:
                    videoRenderWidth = 640;
                    videoRenderHeight = 480;
                    break;
                case 360:
                    videoRenderWidth = 480;
                    videoRenderHeight = 360;
                    break;
                case 240:
                    videoRenderWidth = 320;
                    videoRenderHeight = 240;
                    break;
                case 120:
                    videoRenderWidth = 160;
                    videoRenderHeight = 120;
                    break;
                default:
                    videoRenderWidth = Math.round((float) hResolution / aspect);
                    videoRenderHeight = hResolution;
                    break;
                }
                if (scaling.equals("zoom")) {
                    videoSurfaceWidth = zoomWidth;
                    videoSurfaceHeight = zoomHeight;
                } else if (scaling.equals("crop")) {
                    videoSurfaceWidth = cropWidth;
                    videoSurfaceHeight = cropHeight;
                } else if (scaling.equals("stretch")) {
                    videoSurfaceWidth = stretchWidth;
                    videoSurfaceHeight = stretchHeight;
                } else // scaling.equals( "none" )
                {
                    videoSurfaceWidth = videoRenderWidth;
                    videoSurfaceHeight = videoRenderHeight;
                }
            }
        }
    }

    public void enforceLocale(Activity activity) {
        Configuration config = activity.getBaseContext().getResources().getConfiguration();
        if (!mLocale.equals(config.locale)) {
            config.locale = mLocale;
            activity.getBaseContext().getResources().updateConfiguration(config, null);
        }
    }

    public void changeLocale(final Activity activity) {
        // Get the index of the current locale
        final int currentIndex = ArrayUtils.indexOf(mLocaleCodes, mLocaleCode);

        // Populate and show the language menu
        Builder builder = new Builder(activity);
        builder.setTitle(R.string.menuItem_localeOverride);
        builder.setSingleChoiceItems(mLocaleNames, currentIndex, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                if (which >= 0 && which != currentIndex) {
                    mPreferences.edit().putString(KEY_LOCALE_OVERRIDE, mLocaleCodes[which]).commit();
                    activity.finish();
                    activity.startActivity(activity.getIntent());
                }
            }
        });
        builder.create().show();
    }

    public String getEmulationProfileDefault() {
        return getString(KEY_EMULATION_PROFILE_DEFAULT, DEFAULT_EMULATION_PROFILE_DEFAULT);
    }

    public String getTouchscreenProfileDefault() {
        return getString(KEY_TOUCHSCREEN_PROFILE_DEFAULT, DEFAULT_TOUCHSCREEN_PROFILE_DEFAULT);
    }

    public String getControllerProfileDefault() {
        return getString(KEY_CONTROLLER_PROFILE_DEFAULT, DEFAULT_CONTROLLER_PROFILE_DEFAULT);
    }

    public int getPakType(int player) {
        return getInt(KEYTEMPLATE_PAK_TYPE, player, DEFAULT_PAK_TYPE);
    }

    public boolean getPlayerMapReminder() {
        return getBoolean(KEY_PLAYER_MAP_REMINDER, DEFAULT_PLAYER_MAP_REMINDER);
    }

    public void putEmulationProfileDefault(String value) {
        putString(KEY_EMULATION_PROFILE_DEFAULT, value);
    }

    public void putTouchscreenProfileDefault(String value) {
        putString(KEY_TOUCHSCREEN_PROFILE_DEFAULT, value);
    }

    public void putControllerProfileDefault(String value) {
        putString(KEY_CONTROLLER_PROFILE_DEFAULT, value);
    }

    public void putPakType(int player, int value) {
        putInt(KEYTEMPLATE_PAK_TYPE, player, value);
    }

    public void putPlayerMapReminder(boolean value) {
        putBoolean(KEY_PLAYER_MAP_REMINDER, value);
    }

    private boolean getBoolean(String key, boolean defaultValue) {
        return mPreferences.getBoolean(key, defaultValue);
    }

    private int getInt(String keyTemplate, int index, int defaultValue) {
        String key = String.format(Locale.US, keyTemplate, index);
        return mPreferences.getInt(key, defaultValue);
    }

    private String getString(String key, String defaultValue) {
        return mPreferences.getString(key, defaultValue);
    }

    private void putBoolean(String key, boolean value) {
        mPreferences.edit().putBoolean(key, value).commit();
    }

    private void putInt(String keyTemplate, int index, int value) {
        String key = String.format(Locale.US, keyTemplate, index);
        mPreferences.edit().putInt(key, value).commit();
    }

    private void putString(String key, String value) {
        mPreferences.edit().putString(key, value).commit();
    }

    private Locale createLocale(String code) {
        String[] codes = code.split("_");
        switch (codes.length) {
        case 1: // Language code provided
            return new Locale(codes[0]);
        case 2: // Language and country code provided
            return new Locale(codes[0], codes[1]);
        case 3: // Language, country, and variant code provided
            return new Locale(codes[0], codes[1], codes[2]);
        default: // Invalid input
            return null;
        }
    }

    /**
     * Gets the selected value of a ListPreference, as an integer.
     * 
     * @param preferences  The object containing the ListPreference.
     * @param key          The key of the ListPreference.
     * @param defaultValue The value to use if parsing fails.
     * 
     * @return The value of the selected entry, as an integer.
     */
    private static int getSafeInt(SharedPreferences preferences, String key, int defaultValue) {
        try {
            return Integer.parseInt(preferences.getString(key, String.valueOf(defaultValue)));
        } catch (NumberFormatException ex) {
            return defaultValue;
        }
    }
}