среда, 25 апреля 2012 г.

9. Меню

Итак, в соответствии с планом, мы наконец-то, с чистой совестью, можем заняться кодированием.

9. Меню


В сегодняшней статье мы займемся задачей (2.1) - созданием меню приложения, попутно составив несколько запросов к Content Provider-у и проконтролировав корректность создания БД.

Само по себе, меню добавляется в приложение достаточно просто. Создадим главное меню, содержащее в себе один пункт вложенного меню, для выбора текущего языка интерфейса, переопределив метод onCreateOptionsMenu:

package com.whiterabbit.tags;

...
public class PuzzleActivity extends Activity {

    private static final int BASE_MENU         = 100;
    private static final int BASE_LOCALE_MENU  = 200;

    private static final int MENU_PROFILE_ITEM = 5;
   
    private View view = null;
    private Menu mainMenu = null;
    private Map<Long, String> stringCache = new HashMap<Long, String>();
    private int currentLocale = PuzzleDb.Locales.LOCALE_EN;

    private static final String[] STRINGS_PROJECTION =
            new String[] {
                PuzzleDb.StringValues._ID,
                PuzzleDb.StringValues.COLUMN_NAME_VALUE
        };
   
    private static final String[] LOCALE_PROJECTION =
            new String[] {
                PuzzleDb.Locales._ID,
                PuzzleDb.StringValues.COLUMN_NAME_VALUE
        };

    public String getLocalizedString(long id) {
        String r = stringCache.get(id);
        if (r != null) {
            return r;
        }
        Uri uri = ContentUris.withAppendedId(PuzzleDb.Strings.CONTENT_URI, id);
        Cursor cursor = managedQuery(
                uri,
                STRINGS_PROJECTION,
                PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + " = ?",     
                new String [] {Integer.toString(currentLocale)},
                null
            );
        if (cursor.moveToFirst()) {
            int stringsValueColumn = cursor.getColumnIndex(PuzzleDb.StringValues.COLUMN_NAME_VALUE);
            r = cursor.getString(stringsValueColumn);
        }
        stringCache.put(id, r);
        return r;
    }
   
    public void getLocales(Menu menu) {
        Uri uri = PuzzleDb.Locales.CONTENT_URI;
        Cursor cursor = managedQuery(
                uri,
                LOCALE_PROJECTION,
                "b." + PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + " = ?",     
                new String [] {Integer.toString(currentLocale)},
                null
            );
        for (cursor.moveToFirst();!cursor.isAfterLast();cursor.moveToNext()) {
            int idColumn = cursor.getColumnIndex(PuzzleDb.Puzzles._ID);
            int nameColumn = cursor.getColumnIndex(PuzzleDb.StringValues.COLUMN_NAME_VALUE);
            int id = cursor.getInt(idColumn);
            String name = cursor.getString(nameColumn);
            menu.add(0, id + BASE_LOCALE_MENU, id, name);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        mainMenu = menu;
        super.onCreateOptionsMenu(menu);
        SubMenu newMenu = menu.addSubMenu(BASE_MENU, BASE_MENU + MENU_PROFILE_ITEM, Menu.NONE, getLocalizedString(MENU_PROFILE_ITEM));
        getLocales(newMenu);
        return true;
    }
    ...
}


В методе onCreateOptionsMenu мы создаем один пункт меню, наименование которого получаем при помощи функции getLocalizedString (в зависимости от текущего значения локали) и добавляем в него пункты подменю, методом getLocales.

Методы getLocalizedString и getLocales служат прекрасной иллюстрацией выборки данных, предоставляемых Content Provider-ом. В getLocalizedString мы дополнительно кэшируем полученные строки, с целью минимизации количества обращений к БД.
Доработаем Content Provider таким образом, чтобы он отрабатывал используемые нами запросы:

package com.whiterabbit.tags;

...
public class PuzzleProvider extends ContentProvider {

   ...
   private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
   private static final int STRING_URI_INDICATOR  = 1;
   private static final int LOCALE_URI_INDICATOR  = 2;
   private static final int PROFILE_URI_INDICATOR = 3;
  
   static {
       uriMatcher.addURI(PuzzleDb.AUTHORITY, PuzzleDb.Strings.PATH_STRING + "/#", STRING_URI_INDICATOR);
       uriMatcher.addURI(PuzzleDb.AUTHORITY, PuzzleDb.Locales.PATH_LOCALE, LOCALE_URI_INDICATOR);
   }

   ...
   @Override
   public String getType(Uri uri) {
       switch (uriMatcher.match(uri)) {
            case STRING_URI_INDICATOR:
                return PuzzleDb.Strings.CONTENT_ITEM_TYPE;
               case LOCALE_URI_INDICATOR:
                   return PuzzleDb.Locales.CONTENT_DIR_TYPE;
       }
       return null;
   }

   ...
   @Override
   public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
       SQLiteDatabase db = mOpenHelper.getReadableDatabase();
       switch (uriMatcher.match(uri)) {
          case STRING_URI_INDICATOR:
               return db.rawQuery("select " + PuzzleDb.StringValues._ID +
                                      ", " + PuzzleDb.StringValues.COLUMN_NAME_VALUE +
                                      " from " + PuzzleDb.StringValues.TABLE_NAME +
                                      " where " + PuzzleDb.StringValues.COLUMN_NAME_STRING_ID +
                                      " = " + uri.getPathSegments().get(1) +
                                      " and " + selection, selectionArgs);
           case LOCALE_URI_INDICATOR:
               return db.rawQuery("select a." + PuzzleDb.Locales._ID + " as " + PuzzleDb.Locales._ID + ", " +
                                   "b." + PuzzleDb.StringValues.COLUMN_NAME_VALUE + " as " + PuzzleDb.StringValues.COLUMN_NAME_VALUE + " " +
                                   "from " + PuzzleDb.Locales.TABLE_NAME + " a, " +
                                   PuzzleDb.StringValues.TABLE_NAME + " b " +
                                   "where b." + PuzzleDb.StringValues.COLUMN_NAME_STRING_ID + " = " +
                                   "a." + PuzzleDb.Locales.COLUMN_NAME_STRING_ID + " and " + selection, selectionArgs);
       }
       return null;
   }
   ...
}


Здесь мы создаем UriMatcher, который будет определять тип запроса по URI, и дописываем обработку запросов в getType и query. После запуска приложения и нажатия на кнопку <меню> наблюдаем ... пункт меню с пустым названием :(

Выясняется, что в прошлой статье мы напутали с идентификаторами строковых  ресурсов. В отладке такого рода ошибок хорошо помогает утилита просмотра содержимого БД SQLite (например sqlitebrowser).  Саму БД можно забрать с эмулятора (после того как она создана, в результате  первого к ней обращения) из каталога /data/data/com.whiterabbit.tags/databases/puzzle.db (в перспективе "DDMS" Eclipse).
Удалим БД на эмуляторе и внесем исправления в код создания БД:

package com.whiterabbit.tags;

...
public class PuzzleProvider extends ContentProvider {

   ...
   static class DatabaseHelper extends SQLiteOpenHelper {

       @Override
       public void onCreate(SQLiteDatabase db) {

           ...
           db.execSQL("CREATE TABLE " + PuzzleDb.Strings.TABLE_NAME + " (" +
                   PuzzleDb.Strings._ID + " INTEGER PRIMARY KEY);");
           addString(db, "1"); // English
           addString(db, "2"); // Russian
           addString(db, "3"); // Size X
           addString(db, "4"); // Size Y
           addString(db, "5"); // Language
           addString(db, "11");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Locales.TABLE_NAME + " (" +
                   PuzzleDb.Locales._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Locales.COLUMN_NAME_NAME + " TEXT," +
                   PuzzleDb.Locales.COLUMN_NAME_STRING_ID + " INTEGER," +
                   PuzzleDb.Locales.COLUMN_NAME_IS_DEFAULT + " INTEGER," +
                   PuzzleDb.Locales.COLUMN_NAME_DESCRIPTION + " TEXT);");
           addLocale(db, PuzzleDb.Locales.LOCALE_EN + ", 'en_US', 1, 1");
           addLocale(db, PuzzleDb.Locales.LOCALE_RU + ", 'ru_RU', 2, 0");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.StringValues.TABLE_NAME + " (" +
                   PuzzleDb.StringValues._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + " INTEGER," +
                   PuzzleDb.StringValues.COLUMN_NAME_STRING_ID + " INTEGER," +
                   PuzzleDb.StringValues.COLUMN_NAME_VALUE + " TEXT);");
           addValue(db, "1, 1,  1, 'English'");
           addValue(db, "2, 2,  1, 'Английский'");
           addValue(db, "3, 1,  2, 'Russian'");
           addValue(db, "4, 2,  2, 'Русский'");
           addValue(db, "5, 1,  5, 'Language'");
           addValue(db, "6, 2,  5, 'Язык'");
           addValue(db, "7, 1, 11, 'Donkey'");
           addValue(db, "8, 2, 11, 'Рыжий осел'");
           ...
       }
       ...
}

После запуска пересоздания БД, главное меню содержит пункт "Language", при выборе которого открывается подменю, содержащее пункты "English" и "Russian", при нажатии на которые ничего не происходит.
Добавим реакцию на выбор языка интерфейса, переопределив метод onOptionsItemSelected:

package com.whiterabbit.tags;

...
public class PuzzleActivity extends Activity {

    ...
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int itemId = item.getItemId();
        if (itemId >= BASE_LOCALE_MENU) {
            currentLocale = itemId - BASE_LOCALE_MENU;
            stringCache.clear();
            view.invalidate();
            if (mainMenu != null) {
                mainMenu.clear();
                onCreateOptionsMenu(mainMenu);
            }
            saveProfile();
        }
        return true;
    }
    ...
}

При выборе пункта меню, мы проверяем, используется ли этот пункт для выбора языка приложения, получаем currentLocale из идентификатора пункта меню и сохраняем изменения в профиле. Для перестроения меню мы принудительно его очищаем и самостоятельно вызываем onCreateOptionsMenu (я не знаю насколько такие манипуляции законны, но это работает).
Добавим в PuzzleProvider поддержку сохранения изменений профиля:

package com.whiterabbit.tags;

...
public class PuzzleProvider extends ContentProvider {

   ...
   private static final int PROFILE_URI_INDICATOR = 3;

   static {
       ...
       uriMatcher.addURI(PuzzleDb.AUTHORITY, PuzzleDb.Profiles.PATH_PROFILE, PROFILE_URI_INDICATOR);
   }

   ...
   @Override
   public String getType(Uri uri) {
       switch (uriMatcher.match(uri)) {
               ...
               case PROFILE_URI_INDICATOR:
                   return PuzzleDb.Profiles.CONTENT_DIR_TYPE;
       }
       return null;
   }

   @Override
   public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
       SQLiteDatabase db = mOpenHelper.getWritableDatabase();
       switch (uriMatcher.match(uri)) {
          case PROFILE_URI_INDICATOR:
               return db.update(PuzzleDb.Profiles.TABLE_NAME,
                       values,
                       PuzzleDb.Profiles.COLUMN_NAME_IS_DEFAULT + " = 1",
                       null);
       }
       return 0;
   }
   ...
}

После запуска приложения, можно убедиться, что все работает именно так, как мы и запланировали (правда код локали не загружается при старте приложения из профиля, но эту задачу мы перед собой сегодня и не ставили).

В следующей статье мы продолжим двигаться по плану и реализуем отображение окна подсказки при рестарте головоломки. Код проекта можно получить здесь.

вторник, 24 апреля 2012 г.

8. Снова планирование

Прежде чем продолжать увлеченно кодить наше приложение , хорошо-бы понять что мы хотим получить на выходе.

8. Снова планирование


Распишем желательный алгоритм поведения нашего приложения, при наступлении определенных ключевых событий. Используемая мной ниже нотация не является общепринятой, но я нахожу ее вполне удобной.

1. Запуск Activity (onCreate)
   1.1 Загрузить не закрытую сессию
       a) long sessionId
       b) int profileId
       c) int puzzleId
       d) long positionId
       e) int localeId
   1.2 Если сессия не найдена
       1.2.1 Загрузить профиль по умолчанию
             a) int profileId
             b) int puzzleId
             c) int localeId
       1.2.2 Получить идентификатор начальной позиции для puzzleId
             a) long positionId
       1.2.3 Создать сессию, сохранив текущие параметры
             a) long sessionId
   1.3 Загрузить позицию positionId для головоломки puzzleId
       1.3.1 Загрузить параметры головоломки
             a) Размер игрового поля по вертикали
             b) Размер игрового поля по горизонтали
       1.3.2 Загрузить параметры Item-ов
             a) long itemId
             b) long tagId (принадлежность к Tag-у)
             c) int x (координата по горизонтали)
             d) int x (координата по вертикали)
       1.3.3 Загрузить финальные позиции
             1.3.3.1 Загрузить item-ы каждой финальной позиции для puzzleId
2. Приостановка Activity (onPause)
   2.1 Создать новую позицию
       a) long newPositionId
   2.2 Сохранить координаты всех Item-ов в newPositionId
   2.3 Сохранить newPositionId в текущей сессии sessionId
   2.4 Если старая позиция positionId не связана с puzzleId, удалить ее
       2.4.1 Удалить все Item-ы, связанные с positionId
       2.4.2 Удалить positionId
3. Запуск новой игры (меню)
   3.1 Получить параметры новой игры
       a) newPuzzleId
       b) newPositionId (стартовая позиция)
   3.2 Сохранение параметров, полученных в 3.1
       3.2.1 Сохранить значение newPuzzleId в профиле
       3.2.2 Сохранить значение newPositionId в sessionId
   3.3 Если positionId не связана с какой либо puzzleId, удалить ее (аналогично п.2.4)
   3.4 Загрузить головоломку (аналогично п.1.3)
   3.5 Отобразить экран подсказки
4. Завершение игры (достижение финальной позиции)
   4.1 Сохранить текущую позицию (аналогично п.2)
   4.2 Пометить сессию как закрытую
   4.3 Загрузить список рекордов, выделив текущую сессию
   4.4 Получить начальную позицию для текущей puzzleId
       a) positionId
   4.5 Загрузить головоломку (аналогично п.1.3)
5. Изменение пользовательского профиля (меню)
   3.1 Получить новые параметры
       a) localeId
   3.2 Сохранить новое значение localeId в профиле
   3.3 Перестроить меню

Этот алгоритм описывается "сверху-вниз", то есть сначала составляется список пунктов верхнего уровня, детализируемых впоследствии, по мере необходимости. Перепроверив логику работы алгоритма, приступаем к составлению списка задач:

1. Critical
2. High
   2.1 Создать меню (с поддержкой локализации и возможностью перестроения)
   2.2 Реализовать окно подсказки
   2.3 Реализовать таблицу рекордов
3. Normal
   3.1  Загрузить параметры профиля
   3.2  Загрузить параметры открытой сессии
   3.3  Получить начальную positionId из puzzleId
   3.4  Получить параметры для puzzleId
   3.5  Получить список Item-ов для positionId
   3.6  Получить список финальных positionId для puzzleId
   3.7  Реализовать логику создания сессии
   3.8  Реализовать логику сохранения текущей позиции (п.2)
   3.9  Реализовать логику сохранения параметров сессии
   3.10 Реализовать логику сохранения параметров профиля
   3.11 Реализовать закрытие сессии
4. Low

Мы имеем определенную свободу в назначении приоритетов задачам, но имеет смысл делать более приоритетными задачи, от которых зависит выполнение остальных. 
В последующем, составленный нами список задач может пополняться. Так обнаруженные в процессе работы ошибки имеет смысл отмечать в разделе Critical (если они влияют на базовую функциональность) или Low (в противном случае). Также, в Low могут вноситься задачи по доработкам функциональности, возникшие в процессе работы.

Имея на руках список задач, можно распределить силы, оценить трудозатраты, распределить задачи по итерациям и т. п. В следующей статье мы приступим к реализации задач с приоритетом High и разберемся с созданием меню.

понедельник, 23 апреля 2012 г.

7. Реализация БД

Поскольку в прошлой статье мы спроектировали базу данных, сегодня самое время перенести ее в код.

7. Реализация БД


Придерживаясь рекомендаций Google, мы будем предоставлять данные из БД через Content Provider, который требуется зарегистрировать в AndroidMainfest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.whiterabbit.tags"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="7" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <provider android:name="PuzzleProvider"
            android:authorities="com.WhiteRabbit.provider.Puzzle">
        </provider>
        <activity
            android:name=".PuzzleActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>


Действия по реализации встроенной БД рутинны и однообразны :( Вначале мы создаем в проекте класс PuzzleDb, который будет содержать строковые константы, описывающие все наши таблицы (вообще говоря, можно обойтись и без него, но в этом случае все наименования таблиц и полей будут содержаться непосредственно в коде, что может существенно усложнить последующее сопровождение проекта).

Я не буду приводить листинг всего класса целиком (желающие могут найти его в проекте, выложенном в конце статьи), поскольку он настолько однообразен, что в пору подумать о его автоматической генерации, на основании результата работы нашего CASE-средства (можно распарсить XML-файл или выгружаемые  SQL-скрипты, каким нибудь Perl-скриптом, генерируя непосредственно Java-код). 

package com.whiterabbit.tags;

import android.net.Uri;
import android.provider.BaseColumns;

public final class PuzzleDb {

    private static final String SCHEME = "content://";
    public static final String AUTHORITY = "com.WhiteRabbit.provider.Tags";
   
    private TagsDb() {}
   
    public static final class Locales implements BaseColumns {
       
        public static final String PATH_LOCALE = "locales";
        public static final Uri CONTENT_URI =  Uri.parse(SCHEME + AUTHORITY + "/" + PATH_LOCALE);
        public static final String CONTENT_DIR_TYPE = "vnd.android.cursor.dir/vnd.com.WhiteRabbit.tags.locales";
       
        private Locales() {}
       
        public static final int LOCALE_EN = 1;
        public static final int LOCALE_RU = 2;
       
        public static final String TABLE_NAME = "locale";
        public static final String COLUMN_NAME_NAME = "name";
        public static final String COLUMN_NAME_STRING_ID = "string_id";
        public static final String COLUMN_NAME_DESCRIPTION = "description";
        public static final String COLUMN_NAME_IS_DEFAULT = "is_default";
       
    }
    ...
}


Здесь следует обратить внимание на то, что значение константы AUTHORITY должно совпадать со значением атрибута android:authorities в описании Content Provider-а. Объявив пустой приватный конструктор (чтобы исключить возможность создания экземпляров класса TagsDb), переходим к описанию таблиц БД, используя вложенные классы.

Внутри вложенных классов мы определяем константами наименование таблицы (TABLE_NAME) и всех ее столбцов. Также мы можем определить заранее известные значения (такие как внутренние коды локалей, используемые нашей БД).

Константы PATH_LOCALE, CONTENT_URI, CONTENT_DIR_TYPE и CONTENT_ITEM_TYPE будут использоваться нами при реализации Content Provider-а, пока не обращаем на них внимания. Также как и для TagsDb, создаем приватный конструктор, чтобы исключить возможность инстанционирования класса.

Далее создаем расширение класса android.content.ContentProvider. Содержимое этого класса несколько более интересно и я приведу его полностью: 

package com.whiterabbit.tags;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;

public class PuzzleProvider extends ContentProvider {
 
   private static final String DATABASE_NAME = "puzzle.db";
   private static final int DATABASE_VERSION = 1;
  
   private DatabaseHelper mOpenHelper;
  
   @Override
   public boolean onCreate() {
       mOpenHelper = new DatabaseHelper(getContext());
       return true;
   }

   @Override
   public String getType(Uri uri) {
       // TODO:
       return null;
   }

   @Override
   public Uri insert(Uri uri, ContentValues values) {
       SQLiteDatabase db = mOpenHelper.getWritableDatabase();
       // TODO:
       return null;
   }

   @Override
   public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
       SQLiteDatabase db = mOpenHelper.getWritableDatabase();
       // TODO:
       return 0;
   }
  
   @Override
   public int delete(Uri uri, String selection, String[] selectionArgs) {
       int r = 0;
       SQLiteDatabase db = mOpenHelper.getWritableDatabase();
       // TODO:
       return r;
   }

   @Override
   public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
       SQLiteDatabase db = mOpenHelper.getReadableDatabase();
       // TODO:
       return null;
   }

   static class DatabaseHelper extends SQLiteOpenHelper {

       DatabaseHelper(Context context) {
          super(context, DATABASE_NAME, null, DATABASE_VERSION);
       }
      
       public void addString(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.Strings.TABLE_NAME + "(" +
                   PuzzleDb.Strings._ID + ") values (" + s + ")");
       }
      
       public void addValue(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.StringValues.TABLE_NAME + "(" +
                   PuzzleDb.StringValues._ID + "," +
                   PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + "," +
                   PuzzleDb.StringValues.COLUMN_NAME_STRING_ID + "," +
                   PuzzleDb.StringValues.COLUMN_NAME_VALUE + ") values " +
                   "(" + s + ")");
       }
      
       public void addLocale(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.Locales.TABLE_NAME + "(" +
                   PuzzleDb.Locales._ID + "," +
                   PuzzleDb.Locales.COLUMN_NAME_NAME + "," +
                   PuzzleDb.Locales.COLUMN_NAME_STRING_ID + "," +
                   PuzzleDb.Locales.COLUMN_NAME_DESCRIPTION + ") values " +
                   "(" + s + ")");
       }
      
       public void addPosition(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.Positions.TABLE_NAME + "(" +
                   PuzzleDb.Positions._ID + "," +
                   PuzzleDb.Positions.COLUMN_NAME_IS_PROTECTED + ") values " +
                        "(" + s + ")");
       }
      
       public void addPuzzle(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.Puzzles.TABLE_NAME + "(" +
                   PuzzleDb.Puzzles._ID + "," +
                   PuzzleDb.Puzzles.COLUMN_NAME_STRING_ID + "," +
                   PuzzleDb.Puzzles.COLUMN_NAME_START_POSITION_ID + ") values " +
                        "(" + s + ")");
       }
          
       public void addParam(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.Params.TABLE_NAME + "(" +
                   PuzzleDb.Params._ID + "," +
                   PuzzleDb.Params.COLUMN_NAME_PARAM_TYPE_ID + "," +
                   PuzzleDb.Params.COLUMN_NAME_PUZZLE_ID + "," +
                   PuzzleDb.Params.COLUMN_NAME_VALUE + ") values " +
                        "(" + s + ")");
       }

       public void addEndPos(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.EndPositions.TABLE_NAME + "(" +
                   PuzzleDb.EndPositions._ID + "," +
                   PuzzleDb.EndPositions.COLUMN_NAME_PUZZLE_ID + "," +
                   PuzzleDb.EndPositions.COLUMN_NAME_POSITION_ID + ") values " +
                        "(" + s + ")");
       }
      
       public void addTag(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.Tags.TABLE_NAME + "(" +
                   PuzzleDb.Tags._ID + "," +
                   PuzzleDb.Tags.COLUMN_NAME_PUZZLE_ID + "," +
                   PuzzleDb.Tags.COLUMN_NAME_IX + ") values " +
                        "(" + s + ")");
       }

       public void addItem(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.Items.TABLE_NAME + "(" +
                   PuzzleDb.Items._ID + "," +
                   PuzzleDb.Items.COLUMN_NAME_TAG_ID + "," +
                   PuzzleDb.Items.COLUMN_NAME_IMG_ID + "," +
                   PuzzleDb.Items.COLUMN_NAME_X + "," +
                   PuzzleDb.Items.COLUMN_NAME_Y + ") values " +
                        "(" + s + ")");
       }
      
       public void addTagPos(SQLiteDatabase db, String s) {
           db.execSQL("insert into " + PuzzleDb.TagPositions.TABLE_NAME + "(" +
                   PuzzleDb.TagPositions._ID + "," +
                   PuzzleDb.TagPositions.COLUMN_NAME_TAG_ID + "," +
                   PuzzleDb.TagPositions.COLUMN_NAME_POSITION_ID + "," +
                   PuzzleDb.TagPositions.COLUMN_NAME_X + "," +
                   PuzzleDb.TagPositions.COLUMN_NAME_Y + ") values " +
                        "(" + s + ")");
       }

       @Override
       public void onCreate(SQLiteDatabase db) {

           db.execSQL("CREATE TABLE " + PuzzleDb.Strings.TABLE_NAME + " (" +
                   PuzzleDb.Strings._ID + " INTEGER PRIMARY KEY);");
           addString(db, "1"); // English
           addString(db, "2"); // Russian
           addString(db, "3"); // Size X
           addString(db, "4"); // Size Y
           addString(db, "11");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Locales.TABLE_NAME + " (" +
                   PuzzleDb.Locales._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Locales.COLUMN_NAME_NAME + " TEXT," +
                   PuzzleDb.Locales.COLUMN_NAME_STRING_ID + " INTEGER," +
                   PuzzleDb.Locales.COLUMN_NAME_IS_DEFAULT + " INTEGER," +
                   PuzzleDb.Locales.COLUMN_NAME_DESCRIPTION + " TEXT);");
           addLocale(db, PuzzleDb.Locales.LOCALE_EN + ", 'en_US', 6, 1");
           addLocale(db, PuzzleDb.Locales.LOCALE_RU + ", 'ru_RU', 7, 0");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.StringValues.TABLE_NAME + " (" +
                   PuzzleDb.StringValues._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + " INTEGER," +
                   PuzzleDb.StringValues.COLUMN_NAME_STRING_ID + " INTEGER," +
                   PuzzleDb.StringValues.COLUMN_NAME_VALUE + " TEXT);");
           addValue(db, "1, 1,  1, 'English'");
           addValue(db, "2, 2,  1, 'Английский'");
           addValue(db, "3, 1,  2, 'Russian'");
           addValue(db, "4, 2,  2, 'Русский'");
           addValue(db, "5, 1, 11, 'Donkey'");
           addValue(db, "6, 2, 11, 'Рыжий осел'");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Profiles.TABLE_NAME + " (" +
                   PuzzleDb.Profiles._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Profiles.COLUMN_NAME_LOGIN + " TEXT," +
                   PuzzleDb.Profiles.COLUMN_NAME_LOCALE_ID + " INTEGER," +
                   PuzzleDb.Profiles.COLUMN_NAME_PUZZLE_ID + " INTEGER," +
                   PuzzleDb.Profiles.COLUMN_NAME_IS_DEFAULT + " INTEGER);");
           db.execSQL("insert into " + PuzzleDb.Profiles.TABLE_NAME + "(" +
                   PuzzleDb.Profiles._ID + "," +
                   PuzzleDb.Profiles.COLUMN_NAME_LOGIN + "," +
                   PuzzleDb.Profiles.COLUMN_NAME_LOCALE_ID + "," +
                   PuzzleDb.Profiles.COLUMN_NAME_PUZZLE_ID + "," +
                   PuzzleDb.Profiles.COLUMN_NAME_IS_DEFAULT + ") values " +
                   "(1, 'default', 0, 3, 1)");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Sessions.TABLE_NAME + " (" +
                   PuzzleDb.Sessions._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Sessions.COLUMN_NAME_PROFILE_ID + " INTEGER," +
                   PuzzleDb.Sessions.COLUMN_NAME_PUZZLE_ID + " INTEGER," +
                   PuzzleDb.Sessions.COLUMN_NAME_POSITION_ID + " INTEGER," +
                   PuzzleDb.Sessions.COLUMN_NAME_IS_CURRENT + " INTEGER," +
                   PuzzleDb.Sessions.COLUMN_NAME_START_DATE + " TEXT," +
                   PuzzleDb.Sessions.COLUMN_NAME_END_DATE + " TEXT," +
                   PuzzleDb.Sessions.COLUMN_NAME_IS_CLOSED + " INTEGER);");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.StatTypes.TABLE_NAME + " (" +
                   PuzzleDb.StatTypes._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.StatTypes.COLUMN_NAME_STRING_ID + " INTEGER);");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Positions.TABLE_NAME + " (" +
                   PuzzleDb.Positions._ID + " INTEGER PRIMARY KEY," +
                      PuzzleDb.Positions.COLUMN_NAME_IS_PROTECTED + " INTEGER);");
           addPosition(db, "1, 1");
           addPosition(db, "2, 1");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Puzzles.TABLE_NAME + " (" +
                   PuzzleDb.Puzzles._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Puzzles.COLUMN_NAME_STRING_ID + " INTEGER," +
                   PuzzleDb.Puzzles.COLUMN_NAME_START_POSITION_ID + " INTEGER);");
           addPuzzle(db, "1, 1, 1");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.ParamTypes.TABLE_NAME + " (" +
                   PuzzleDb.ParamTypes._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.ParamTypes.COLUMN_NAME_STRING_ID + " INTEGER);");
           db.execSQL("insert into " + PuzzleDb.ParamTypes.TABLE_NAME + "(" +
                   PuzzleDb.ParamTypes._ID + "," +
                   PuzzleDb.ParamTypes.COLUMN_NAME_STRING_ID + ") values " +
                   "(" + Integer.toString(PuzzleDb.ParamTypes.SizeX) + ", 3)");
           db.execSQL("insert into " + PuzzleDb.ParamTypes.TABLE_NAME + "(" +
                   PuzzleDb.ParamTypes._ID + "," +
                   PuzzleDb.ParamTypes.COLUMN_NAME_STRING_ID + ") values " +
                   "(" + Integer.toString(PuzzleDb.ParamTypes.SizeY) + ", 4)");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Params.TABLE_NAME + " (" +
                   PuzzleDb.Params._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Params.COLUMN_NAME_PARAM_TYPE_ID + " INTEGER," +
                   PuzzleDb.Params.COLUMN_NAME_PUZZLE_ID + " INTEGER," +
                   PuzzleDb.Params.COLUMN_NAME_VALUE + " TEXT);");
           addParam(db, "1, " + Integer.toString(PuzzleDb.ParamTypes.SizeX) + ", 1, 4");
           addParam(db, "2, " + Integer.toString(PuzzleDb.ParamTypes.SizeY) + ", 1, 5");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Stats.TABLE_NAME + " (" +
                   PuzzleDb.Stats._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Stats.COLUMN_NAME_STAT_TYPE_ID + " INTEGER," +
                   PuzzleDb.Stats.COLUMN_NAME_SESSION_ID + " INTEGER," +
                   PuzzleDb.Stats.COLUMN_NAME_VALUE + " INTEGER);");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.EndPositions.TABLE_NAME + " (" +
                   PuzzleDb.EndPositions._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.EndPositions.COLUMN_NAME_PUZZLE_ID + " INTEGER," +
                   PuzzleDb.EndPositions.COLUMN_NAME_POSITION_ID + " INTEGER);");
           addEndPos(db, "1, 1, 2");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Tags.TABLE_NAME + " (" +
                   PuzzleDb.Tags._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Tags.COLUMN_NAME_PUZZLE_ID + " INTEGER," +
                   PuzzleDb.Tags.COLUMN_NAME_IX + " INTEGER);");
           addTag(db, "1, 1, 1");
           addTag(db, "2, 1, 2");
           addTag(db, "3, 1, 3");
           addTag(db, "4, 1, 4");
           addTag(db, "5, 1, 5");
           addTag(db, "6, 1, 6");
           addTag(db, "7, 1, 7");
           addTag(db, "8, 1, 8");
           addTag(db, "9, 1, 9");
           addTag(db, "10, 1, 10");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.Items.TABLE_NAME + " (" +
                   PuzzleDb.Items._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.Items.COLUMN_NAME_TAG_ID + " INTEGER," +
                   PuzzleDb.Items.COLUMN_NAME_IMG_ID + " INTEGER," +
                   PuzzleDb.Items.COLUMN_NAME_X + " INTEGER," +
                   PuzzleDb.Items.COLUMN_NAME_Y + " INTEGER);");
           addItem(db, "1,  1,  0, 0, 0");
           addItem(db, "2,  1,  0, 0, 1");
           addItem(db, "3,  2,  0, 0, 0");
           addItem(db, "4,  2,  0, 1, 0");
           addItem(db, "5,  2,  0, 0, 1");
           addItem(db, "6,  2,  0, 1, 1");
           addItem(db, "7,  3,  0, 0, 0");
           addItem(db, "8,  3,  0, 0, 1");
           addItem(db, "9,  4,  0, 0, 0");
           addItem(db, "10, 4,  0, 0, 1");
           addItem(db, "11, 5,  0, 0, 0");
           addItem(db, "13, 5,  0, 1, 0");
           addItem(db, "14, 6,  0, 0, 0");
           addItem(db, "15, 6,  0, 0, 1");
           addItem(db, "16, 7,  0, 0, 0");
           addItem(db, "17, 8,  0, 0, 0");
           addItem(db, "18, 9,  0, 0, 0");
           addItem(db, "19, 10, 0, 0, 0");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.TagPositions.TABLE_NAME + " (" +
                   PuzzleDb.TagPositions._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.TagPositions.COLUMN_NAME_TAG_ID + " INTEGER," +
                   PuzzleDb.TagPositions.COLUMN_NAME_POSITION_ID + " INTEGER," +
                   PuzzleDb.TagPositions.COLUMN_NAME_X + " INTEGER," +
                   PuzzleDb.TagPositions.COLUMN_NAME_Y + " INTEGER);");
           addTagPos(db, "1,  1,  1, 1, 1");
           addTagPos(db, "2,  2,  1, 2, 1");
           addTagPos(db, "3,  3,  1, 4, 1");
           addTagPos(db, "4,  4,  1, 1, 3");
           addTagPos(db, "5,  5,  1, 2, 3");
           addTagPos(db, "6,  6,  1, 4, 3");
           addTagPos(db, "7,  7,  1, 2, 4");
           addTagPos(db, "8,  8,  1, 3, 4");
           addTagPos(db, "9,  9,  1, 1, 5");
           addTagPos(db, "10, 10, 1, 4, 5");
           addTagPos(db, "11, 2,  2, 2, 4");
          
           db.execSQL("CREATE TABLE " + PuzzleDb.SolutionSteps.TABLE_NAME + " (" +
                   PuzzleDb.SolutionSteps._ID + " INTEGER PRIMARY KEY," +
                   PuzzleDb.SolutionSteps.COLUMN_NAME_SESSION_ID + " INTEGER," +
                   PuzzleDb.SolutionSteps.COLUMN_NAME_ORD_NUM + " INTEGER," +
                   PuzzleDb.SolutionSteps.COLUMN_NAME_TAG_ID + " INTEGER," +
                   PuzzleDb.SolutionSteps.COLUMN_NAME_DX + " INTEGER," +
                   PuzzleDb.SolutionSteps.COLUMN_NAME_DY + " INTEGER);");
       }

       @Override
       public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
           // TODO:
       }
   }
}


Наименование класса должно совпадать с именем Content Provider-а объявленного в AndroidManifest.xml. Также следует обратить внимание на константы  DATABASE_NAME и DATABASE_VERSION. В DATABASE_NAME задается имя файла, в  котором будет храниться БД (в каталоге /data/data/com.WhiteRabbit.tags/databases на устройстве или эмуляторе). В DATABASE_VERSION задается версия нашей БД. При любых изменениях БД номер следует увеличивать, чтобы Android мог отследить необходимость применения изменений БД.

Большинство методов, унаследованных от ContentProvider пока оставляем пустыми (ими мы займемся в следующих статьях). В метод onCreate внутреннего класса DatabaseHelper помещаем код, выполняющий создание БД, держа перед глазами ER-диаграмму, разработанную в предыдущей статье.

К сожалению, пока мы не можем отладить этот код. Он будет вызван только при фактическом обращении к БД, а в нашем проекте пока нет кода выполняющего такое обращение. Разработкой этого кода мы займемся в последующих статьях.