В ходе написания программы возникло некоторое количество проблем.
1. Организация поддержки татарского языка.
Языковые настройки являются частью конфигурации устройства - набор характеристик, описывающих текущее состояние конкретного устройства. Android предоставляет конфигурационные квалификаторы для разных языков, существенно упрощающие локализацию: достаточно создать подкаталоги ресурсов с разными языковыми квалификаторами и разместить в них альтернативные ресурсы. Система ресурсов Android делает все остальное. Языковые квалификаторы позаимствованы из кодов ISO 639-1. Существует квалификатор и для татарского языка – квалификатор -tt. На рис. 12 представлен каталог ресурсов приложения.
Рис. 12. Каталог ресурсов.
Далее, при изменении настроек языка нужно изменять локаль. За это отвечает код в классе Settings из листинга 1.
Листинг 1. Изменение конфигурации устройства при изменении настроек приложения (Settings.java)
public class Settings {
...
public enum LANGUAGE {
Russian, Tatar
};
...
public void setLanguage(LANGUAGE lang) {
mLanguage = lang;
setLocale();
saveSettings();
}
private void setLocale() {
String qualifier;
switch (mLanguage) {
case Russian:
qualifier = "ru";
break;
case Tatar:
qualifier = "tt";
break;
default:
qualifier = mDefaultQualifier;
break;
}
Locale locale = new Locale(qualifier);
Locale. setDefault (locale);
android.content.res.Configuration config = new android.content.res.Configuration();
config.locale = locale;
mAppContext.getResources().updateConfiguration(config,
mAppContext.getResources().getDisplayMetrics());
}
...
}
Вроде бы, проблема решена. Однако если повернуть устройство, тем самым изменив его конфигурацию, язык в приложении станет тем же, что и на устройстве. Это связано с тем, что Android уничтожает текущую активность и создает новую при каждом изменении конфигурации времени выполнения, чтобы обеспечить оптимальный подбор ресурсов для новой конфигурации. Одним из решений является переопределения метода жизненного цикла onCreate(…), добавив в метод изменение локали. Это кажется оптимальным решением, ведь в приложении всего две активности. Однако со временем при усовершенствовании приложения их может стать больше. И чтобы не добавлять один и тот же код в каждую из них, был создан класс LocaleSettingFragmentActivity, который расширяют все активности приложения (листинг 2).
Листинг 2. (LocaleSettingFragmentActivity.java)
public abstract class LocaleSettingFragmentActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super. onCreate(savedInstanceState);
Settings. get (this).set();
}
}
В методе set() класса Settings вызывается метод setLocale() того же класса.
Наконец, так как диалоговое окно выбора языка вызывается из фрагмента TopicsFragment, необходимо каким-то образом обновить фрагмент после выхода из меню. Для этого фрагмент отсоединяется и присоединяется к менеджеру фрагментов активности, тем самым заставляя Android заново построить фрагмент (листинг 3).
Листинг 3. Обновление фрагмента (TopicsFragment.java)
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode!= Activity. RESULT_OK)
return;
if (requestCode == REQUEST_LANGUAGE) {
getActivity().getSupportFragmentManager().beginTransaction()
.detach(this).attach(this).commit();
}
...
}
2. Запись собственного звука
Для записи звука с диктофона устройства использован экземпляр класса MediaRecorder. В листинге 4 представлены методы класса UserSoundFragment, отвечающие за начало и остановку записи.
Листинг 4. Запись звука (UserSoundFragment.java)
public class UserSoundFragment extends DialogFragment{
...
private boolean mRecordingStarted;
private MediaRecorder mRecorder;
private String mSoundFileName;
private static final int RECORDING_MAX_LENGTH_IN_SECONDS = 3;
...
private void startRecording() {
mRecordingStarted = true;
...
if (mSoundFileName == null) {
mSoundFileName = UUID. randomUUID ().toString() + ".3gp";
getArguments().putString(EXTRA_SOUND_FILENAME, mSoundFileName);
}
String soundFileAbsolutePath = Settings. get (getActivity())
.getApplicationExternalStorage() + "/" + mSoundFileName;
mRecorder = new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource. MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat. THREE_GPP);
mRecorder.setOutputFile(soundFileAbsolutePath);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder. AMR_NB);
boolean preparationSuccess = true;
try {
mRecorder.prepare();
} catch (IOException e) {
Log. e (TAG, "recorder prepare() failed");
preparationSuccess = false;
}
if (preparationSuccess) {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
stopRecording();
}
}, RECORDING_MAX_LENGTH_IN_SECONDS * 1000);
mRecorder.start();
} else {
releaseRecorder();
stopRecording();
}
}
private void stopRecording() {
mRecordingStarted = false;
updateRecordButton();
if (mRecorder!= null) {
mRecorder.stop();
releaseRecorder();
}
...
}
private void releaseRecorder() {
if (mRecorder!= null) {
mRecorder. release ();
mRecorder = null;
}
}
}
Звук записывается в файл на внешней памяти. В листинге 5 продемонстрирован код, позволяющий сделать это.
Листинг 5. Доступ к внешней памяти (Settings.java)
public class Settings {
...
private static final String APPLICATION_FOLDER_NAME = "FlashCards";
...
public String getApplicationExternalStorage() {
File appFolder = new File(Environment. getExternalStorageDirectory ().getAbsolutePath()
+ "/" + APPLICATION_FOLDER_NAME);
appFolder.mkdir();
return appFolder.getAbsolutePath();
}
}
Чтоб получить доступ к диктофону и внешней памяти устройства, следует запросить для приложения право на их использование, добавив в манифест элемент uses-permission. Манифест (manifest) представляет собой файл XML с метаданными, описывающими приложение для ОС Android (рис. 13).
Рис. 13. Файл AndroidManifest.xml.
3. Сохранение настроек и записанных звуков
Каждое приложение на устройстве Android имеет каталог в своей песочнице (sandbox). Хранение файлов в песочнице защищает их от других приложений и даже любопытных глаз пользователей (если только устройство не было «взломано» — в этом случае пользователь сможет делать все, что ему заблагорассудится). Песочница каждого приложения представляет собой подкаталог каталога /data/data, имя которого соответствует имени пакета приложения.
Поддержка долгосрочного хранения данных в приложении включает два процесса: сохранение данных в файловой системе и их загрузка при запуске приложения.
Каждый процесс состоит из двух фаз. При сохранении данные сначала преобразуются в формат хранения, после чего результат записывается в файл. При загрузке все происходит наоборот: отформатированные данные сначала читаются из файла, а затем разбираются в формат, с которым работает приложение. Для приложения FlashCards форматом хранения является формат JSON, а операции чтения и записи файлов осуществляются методами ввода-вывода класса Android Context. Формат JSON (JavaScript Object Notation) стал популярным в последнее время, особенно в области веб-служб. Android включает стандартный пакет org.json, классы которого предоставляют средства для создания и разбора файлов в формате JSON.
Механика создания и разбора объектов модели в формате JSON делегирована классу FlashCardsJSONSerializer. В листинге 6 приведен процесс сохранения и загрузки настроек.
public class FlashCardsJSONSerializer {
private Context mContext;
public FlashCardsJSONSerializer(Context context) {
mContext = context;
}
public void saveSettings(String fileName) throws JSONException, IOException {
Writer writer = null;
try {
OutputStream out = mContext.openFileOutput(fileName,
Context. MODE_PRIVATE);
writer = new OutputStreamWriter(out);
writer.write(Settings. get (mContext).settingsToJSON().toString());
} finally {
if (writer!= null)
writer.close();
}
}
public JSONObject loadSettings(String fileName) throws IOException,
JSONException {
BufferedReader reader = null;
try {
InputStream in = mContext.openFileInput(fileName);
reader = new BufferedReader(new InputStreamReader(in));
StringBuilder jsonString = new StringBuilder();
String line = null;
while ((line = reader.readLine())!= null) {
jsonString.append(line);
}
JSONArray array = (JSONArray) new JSONTokener(jsonString.toString())
.nextValue();
return array.getJSONObject(0);
} catch (FileNotFoundException e) {
} finally {
if (reader!= null)
reader.close();
}
return null;
}
}
Далее необходимо добавить поддержку сериализации JSON в классе Settings (листинг 6).
Листинг 6. Реализация сериализации JSON(Settings.java)
public class Settings {
...
private FlashCardsJSONSerializer mSerializer;
private static final String SETTINGS_FILENAME = "settings.json";
private static final String JSON_LANGUAGE = "language";
private static final String JSON_SOUND_ON = "sound_on";
private static final String JSON_CARDTITLE_VISIBLE = "cardtitle_visible";
private static final String JSON_SLIDESHOW_INTERVAL = "slideshow_interval";
private static final String JSON_SLIDESHOW_FROMTHEBEGINNING = "slideshow_fromthebeginning";
public enum LANGUAGE {
Russian, Tatar
};
private LANGUAGE mLanguage;
private boolean mSoundOn;
private boolean mCardTitleVisible;
private int mSlideShowInterval; // in milliseconds
private int mMaxSlideShowInterval = 5000;
private int mMinSlideShowInterval = 500;
private boolean mSlideShowFromTheBeginning;
...
public JSONObject settingsToJSON() throws JSONException {
JSONObject json = new JSONObject();
json.put(JSON_LANGUAGE, mLanguage);
json.put(JSON_SOUND_ON, mSoundOn);
json.put(JSON_CARDTITLE_VISIBLE, mCardTitleVisible);
json.put(JSON_SLIDESHOW_INTERVAL, mSlideShowInterval);
json.put(JSON_SLIDESHOW_FROMTHEBEGINNING, mSlideShowFromTheBeginning);
return json;
}
...
}
Осталось добавить инициирование сохранения и загрузки данных в Settings (листинг 7).
Листинг 7. Инициирование сохранения и загрузки данных(Settings.java)
public class Settings {
private static Settings sSettings;
private Context mAppContext;
private static final String TAG = "Settings";
...
private FlashCardsJSONSerializer mSerializer;
private static final String SETTINGS_FILENAME = "settings.json";
private static final String JSON_LANGUAGE = "language";
private static final String JSON_SOUND_ON = "sound_on";
private static final String JSON_CARDTITLE_VISIBLE = "cardtitle_visible";
private static final String JSON_SLIDESHOW_INTERVAL = "slideshow_interval";
private static final String JSON_SLIDESHOW_FROMTHEBEGINNING = "slideshow_fromthebeginning";
public enum LANGUAGE {
Russian, Tatar
};
private LANGUAGE mLanguage;
private LANGUAGE mDefaultLanguage = LANGUAGE. Russian;
private String mDefaultQualifier = "ru";
private boolean mSoundOn;
private boolean mDefaultSoundOn = true;
private boolean mCardTitleVisible;
private boolean mDefaultCardTitleVisible = true;
private int mSlideShowInterval; // in millisec
private int mMaxSlideShowInterval = 5000;
private int mMinSlideShowInterval = 500;
private int mDefaultSlideShowInterval = 1000;
private boolean mSlideShowFromTheBeginning;
private boolean mDefaultSlideShowFromTheBeginning = false;
public static Settings get(Context c) {
if (sSettings == null) {
sSettings = new Settings(c.getApplicationContext());
}
return sSettings;
}
private Settings(Context c) {
mAppContext = c;
mSerializer = new FlashCardsJSONSerializer(mAppContext);
loadSettings();
}
public void set() {
setLocale();
}
private boolean saveSettings() {
try {
mSerializer.saveSettings(SETTINGS_FILENAME);
return true;
} catch (Exception e) {
Log. e (TAG, "Error saving settings: ", e);
return false;
}
}
private void loadSettings() {
setDefault();
try {
JSONObject json = mSerializer.loadSettings(SETTINGS_FILENAME);
mLanguage = LANGUAGE. valueOf (json.getString(JSON_LANGUAGE));
mCardTitleVisible = json.getBoolean(JSON_CARDTITLE_VISIBLE);
mSlideShowInterval = json.getInt(JSON_SLIDESHOW_INTERVAL);
mSoundOn = json.getBoolean(JSON_SOUND_ON);
mSlideShowFromTheBeginning = json
.getBoolean(JSON_SLIDESHOW_FROMTHEBEGINNING);
} catch (Exception e) {
Log. e (TAG, "Error loading settings: ", e);
}
}
private void setDefault() {
mLanguage = mDefaultLanguage;
mSlideShowInterval = mDefaultSlideShowInterval;
mSoundOn = mDefaultSoundOn;
mSlideShowFromTheBeginning = mDefaultSlideShowFromTheBeginning;
mCardTitleVisible = mDefaultCardTitleVisible;
}
...
}
Сохранение информации о записанных звуках реализовано абсолютно аналогично.