1 /*** 2 * Ambient - A music player for the Android platform 3 Copyright (C) 2007 Martin Vysny 4 5 This program is free software: you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published by 7 the Free Software Foundation, either version 3 of the License, or 8 (at your option) any later version. 9 10 This program is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with this program. If not, see <http://www.gnu.org/licenses/>. 17 */ 18 package sk.baka.ambient.library; 19 20 import java.io.FileNotFoundException; 21 import java.util.ArrayList; 22 import java.util.Collections; 23 import java.util.EnumMap; 24 import java.util.HashMap; 25 import java.util.Iterator; 26 import java.util.List; 27 import java.util.Map; 28 import java.util.Set; 29 import java.util.Map.Entry; 30 31 import sk.baka.ambient.collection.CategoryEnum; 32 import sk.baka.ambient.collection.TrackMetadataBean; 33 import sk.baka.ambient.collection.TrackOriginEnum; 34 import android.content.ContentValues; 35 import android.content.Context; 36 import android.database.Cursor; 37 import android.database.sqlite.SQLiteDatabase; 38 39 /*** 40 * The database backend for the library. Handles database operations. 41 * 42 * @author Martin Vysny 43 */ 44 public final class DBStrategy { 45 /*** 46 * The database handle. 47 */ 48 private SQLiteDatabase db; 49 50 /*** 51 * The DB name. 52 */ 53 private static final String DATABASE_NAME = "Ambient"; 54 55 /*** 56 * The database version. 57 */ 58 public static final int DATABASE_VERSION = 4; 59 60 /*** 61 * Creates the backend instance and opens/creates the database. 62 * 63 * @param ctx 64 * the context. 65 * @throws FileNotFoundException 66 * if the database fails to both open and create. 67 */ 68 DBStrategy(final Context ctx) throws FileNotFoundException { 69 final boolean dbExists = dbExists(ctx); 70 db = ctx 71 .openOrCreateDatabase(DATABASE_NAME, Context.MODE_PRIVATE, null); 72 final int version = db.getVersion(); 73 if (version < 0) 74 throw new FileNotFoundException("Error getting db version: " 75 + version); 76 if ((version != DATABASE_VERSION) || (!dbExists)) { 77 initializeDB(); 78 db.setVersion(DATABASE_VERSION); 79 } 80 } 81 82 private static boolean dbExists(final Context ctx) { 83 final String[] dblist = ctx.databaseList(); 84 if (dblist == null) 85 return false; 86 for (final String dbname : dblist) { 87 if (DATABASE_NAME.equals(dbname)) 88 return true; 89 } 90 return false; 91 } 92 93 /*** 94 * Creates all tables. 95 */ 96 private void initializeDB() { 97 getDb().execSQL("drop table if exists tracks_genres;"); 98 getDb().execSQL("drop table if exists tracks;"); 99 getDb().execSQL("drop table if exists genres;"); 100 getDb().execSQL( 101 "create table tracks " 102 + " (id integer primary key autoincrement, " 103 + "origin integer not null, " 104 + "title text, artist text, composer text, " 105 + "album text, genre text, trackNumber text, " 106 + "location text not null, length integer not null, " 107 + "bitrate integer not null, fileSize long not null, " 108 + "yearReleased text, frequency integer not null, " 109 + "buy_url text, license text, " 110 + "artist_url text, artist_desc text);"); 111 getDb().execSQL("create index idx_lib_title on tracks (title);"); 112 getDb().execSQL("create index idx_lib_artist on tracks (artist);"); 113 getDb().execSQL("create index idx_lib_album on tracks (album);"); 114 getDb().execSQL("create index idx_lib_year on tracks (yearReleased);"); 115 getDb().execSQL( 116 "create index idx_lib_tracknum on tracks (trackNumber);"); 117 getDb().execSQL("create index idx_lib_origin on tracks (origin);"); 118 getDb().execSQL( 119 "create table genres " 120 + "(id integer primary key autoincrement," 121 + "name text unique not null);"); 122 getDb().execSQL("create index idx_gen_genre on genres (name);"); 123 getDb() 124 .execSQL( 125 "create table tracks_genres " 126 + "(trackid integer not null, genreid integer not null);"); 127 getDb().execSQL( 128 "create index idx_tg_trackid on tracks_genres (trackid);"); 129 getDb().execSQL( 130 "create index idx_tg_genreid on tracks_genres (genreid);"); 131 } 132 133 /*** 134 * Closes the database and releases all resources. 135 */ 136 void close() { 137 db.close(); 138 db = null; 139 } 140 141 /*** 142 * Returns the initialized database instance. 143 * 144 * @return database instance, never <code>null</code>. 145 * @throws IllegalStateException 146 * if the database was closed. 147 */ 148 SQLiteDatabase getDb() { 149 if (db == null) 150 throw new IllegalStateException("DB is closed"); 151 return db; 152 } 153 154 /*** 155 * Removes all data from all tables. 156 * 157 * @param storage 158 * the storage to clean. If <code>null</code> then all tracks and 159 * genres are removed. 160 */ 161 void clean(final TrackOriginEnum storage) { 162 if (storage == null) { 163 getDb().delete("tracks_genres", null, null); 164 getDb().delete("tracks", null, null); 165 getDb().delete("genres", null, null); 166 } else { 167 getDb().execSQL( 168 "delete from tracks_genres where trackid in " 169 + "(select id from tracks where origin=" 170 + storage.ordinal() + ");"); 171 getDb().execSQL( 172 "delete from tracks where origin=" + storage.ordinal() 173 + ";"); 174 } 175 } 176 177 /*** 178 * Searches for given genre and returns its id. 179 * 180 * @param genre 181 * the genre to search for, must not be <code>null</code>. 182 * @return id of the genre or <code>-1</code> if given genre does not yet 183 * exist. 184 */ 185 public int getGenreId(final String genre) { 186 final Cursor c = getDb().rawQuery("select id from genres where name=?", 187 new String[] { genre }); 188 int result = -1; 189 if (c.moveToFirst()) { 190 result = c.getInt(0); 191 } 192 c.close(); 193 return result; 194 } 195 196 /*** 197 * Registers new genre and returns its id. The genre must be unique! 198 * 199 * @param name 200 * the genre name, must not be <code>null</code>. 201 * @return id. 202 */ 203 long registerNewGenre(final String name) { 204 final ContentValues values = new ContentValues(); 205 values.put("name", name); 206 return getDb().insert("genres", null, values); 207 } 208 209 /*** 210 * Registers a new track with given genres. 211 * 212 * @param track 213 * the track to register. 214 * @param genreIds 215 * list of genres for this track. 216 * @return track's new ID. 217 */ 218 long registerNewTrack(final TrackMetadataBean track, 219 final Set<Long> genreIds) { 220 final ContentValues values = LibraryUtils.trackBeanToValues(track); 221 final long result = getDb().insert("tracks", null, values); 222 final ContentValues mapping = new ContentValues(); 223 mapping.put("trackid", result); 224 for (final Long genreId : genreIds) { 225 mapping.put("genreid", genreId); 226 getDb().insert("tracks_genres", null, mapping); 227 } 228 return result; 229 } 230 231 /*** 232 * Returns matchable strings from existing tracks for given criteria values. 233 * For example, {@link CategoryEnum#Genre} returns all genres. Returns 234 * ordered list. 235 * 236 * @param crit 237 * the criteria to search for 238 * @param criteria 239 * the criteria map. All criteria combines using the AND 240 * operator. Must not contain the <code>crit</code> parameter. 241 * May be <code>null</code> - this is equal to an empty map. 242 * @return list of all criteria, retrievable via 243 * <code>Cursor.getString(0)</code>. 244 */ 245 public Cursor getCriteriaList(final CategoryEnum[] crit, 246 final EnumMap<CategoryEnum, String> criteria) { 247 final StringBuilder b = new StringBuilder(); 248 for (int i = 0; i < crit.length; i++) { 249 if (i != 0) { 250 b.append(","); 251 } 252 b.append(COLUMN.get(crit[i])); 253 } 254 return getCriteriaCursor(b.toString(), convertCriteria(criteria), 255 false, true, b.toString()); 256 } 257 258 /*** 259 * Searches tracks. 260 * 261 * @param criteria 262 * the criteria map. All criteria combines using the AND 263 * operator. May be <code>null</code> - this is equal to an empty 264 * map. When using the {@link CategoryEnum#Origin} criteria, use 265 * {@link TrackOriginEnum#toDBString()} as value. 266 * @param orderBy 267 * the order by clause, if <code>null</code> then ordering is 268 * undefined. 269 * @param like 270 * if <code>true</code> then given strings will be compared using 271 * the LIKE operator. Note that the values are automatically 272 * enclosed in %%. 273 * @return cursor with items convertible to {@link TrackMetadataBean} using 274 * {@link LibraryUtils#cursorToTrackBean(Cursor)}. Use 275 * {@link LibraryUtils#pollTracks(Cursor)} to get all results. 276 */ 277 public Cursor findByCriteria(final Map<CategoryEnum, String> criteria, 278 final boolean like, final String orderBy) { 279 return getCriteriaCursor(LibraryUtils.COLUMNS, 280 convertCriteria(criteria), like, true, orderBy); 281 } 282 283 /*** 284 * Searches tracks. 285 * 286 * @param criteria 287 * the criteria map. All criteria are combined using the required 288 * operator (depending on the value of the <code>and</code> 289 * argument). May be <code>null</code> - this is equal to an 290 * empty map. Several values for a single criteria are combined 291 * depending on the value of the <code>and</code> parameter. When 292 * using the {@link CategoryEnum#Origin} criteria, use 293 * {@link TrackOriginEnum#toDBString()} as value. 294 * @param like 295 * if <code>true</code> then given strings will be compared using 296 * the LIKE operator. Note that the values are automatically 297 * enclosed in %%. 298 * @param and 299 * If <code>true</code> then the criteria is combined using the 300 * AND operator. If <code>false</code> then the criteria is 301 * combined using the OR operator. 302 * @param orderBy 303 * the order by clause, if <code>null</code> then ordering is 304 * undefined. 305 * @return cursor with items convertible to {@link TrackMetadataBean} using 306 * {@link LibraryUtils#cursorToTrackBean(Cursor)}. Use 307 * {@link LibraryUtils#pollTracks(Cursor)} to get all results. 308 */ 309 public Cursor findByCriteria( 310 final EnumMap<CategoryEnum, List<String>> criteria, 311 final boolean like, final boolean and, final String orderBy) { 312 return getCriteriaCursor(LibraryUtils.COLUMNS, criteria, like, and, 313 orderBy); 314 } 315 316 /*** 317 * Converts a simple criteria to a complex criteria type. 318 * 319 * @param map 320 * the simple criteria. 321 * @return the complex criteria type. 322 */ 323 public static EnumMap<CategoryEnum, List<String>> convertCriteria( 324 final Map<? extends CategoryEnum, ? extends String> map) { 325 final EnumMap<CategoryEnum, List<String>> result = new EnumMap<CategoryEnum, List<String>>( 326 CategoryEnum.class); 327 for (final Entry<? extends CategoryEnum, ? extends String> entry : map 328 .entrySet()) { 329 final CategoryEnum key = entry.getKey(); 330 final String value = entry.getValue(); 331 result.put(key, Collections.singletonList(value)); 332 } 333 return result; 334 } 335 336 /*** 337 * Finds all tracks. 338 * 339 * @return cursor with items convertible to {@link TrackMetadataBean} using 340 * {@link LibraryUtils#cursorToTrackBean(Cursor)}. Use 341 * {@link LibraryUtils#pollTracks(Cursor)} to get all results. 342 */ 343 @SuppressWarnings("unchecked") 344 public Cursor findAll() { 345 return getCriteriaCursor(LibraryUtils.COLUMNS, Collections.EMPTY_MAP, 346 false, true, null); 347 } 348 349 /*** 350 * Performs a raw query on the track table. 351 * 352 * @param where 353 * the where clausule 354 * @param selectionArgs 355 * optional selection argument values, may be <code>null</code>. 356 * @return track query cursor with items convertible to 357 * {@link TrackMetadataBean} using 358 * {@link LibraryUtils#cursorToTrackBean(Cursor)}. Use 359 * {@link LibraryUtils#pollTracks(Cursor)} to get all results. 360 */ 361 public Cursor rawTrackQuery(final String where, final String[] selectionArgs) { 362 final StringBuilder select = new StringBuilder(); 363 select.append("select distinct "); 364 select.append(LibraryUtils.COLUMNS); 365 select.append(" from tracks"); 366 if (where != null) { 367 select.append(" where "); 368 select.append(where); 369 } 370 final Cursor result = getDb() 371 .rawQuery(select.toString(), selectionArgs); 372 return result; 373 } 374 375 private static final Map<CategoryEnum, String> COLUMN = new EnumMap<CategoryEnum, String>( 376 CategoryEnum.class); 377 static { 378 COLUMN.put(CategoryEnum.Album, "album"); 379 COLUMN.put(CategoryEnum.Artist, "artist"); 380 COLUMN.put(CategoryEnum.Genre, "name"); 381 COLUMN.put(CategoryEnum.Origin, "origin"); 382 COLUMN.put(CategoryEnum.Title, "title"); 383 COLUMN.put(CategoryEnum.YearReleased, "yearReleased"); 384 } 385 386 private static final String getTableName(final CategoryEnum cat) { 387 return cat == CategoryEnum.Genre ? "genres" : "tracks"; 388 } 389 390 /*** 391 * Selects given columns for given criteria. 392 * 393 * @param columns 394 * columns to select. 395 * @param criteria 396 * the criteria map. All criteria are combined using the required 397 * operator (depending on the value of the <code>and</code> 398 * argument). May be <code>null</code> - this is equal to an 399 * empty map. Several values for a single criteria are combined 400 * depending on the value of the <code>and</code> parameter. When 401 * using the {@link CategoryEnum#Origin} criteria, use 402 * {@link TrackOriginEnum#toDBString()} as value. 403 * @param orderBy 404 * the order by clause, if <code>null</code> then ordering is 405 * undefined. 406 * @param like 407 * if <code>true</code> then given strings will be compared using 408 * the LIKE operator. Note that the values are automatically 409 * enclosed in %%. 410 * @param and 411 * If <code>true</code> then the criteria is combined using the 412 * AND operator. If <code>false</code> then the criteria is 413 * combined using the OR operator. 414 * @return cursor with items convertible to {@link TrackMetadataBean} using 415 * {@link LibraryUtils#cursorToTrackBean(Cursor)}. Use 416 * {@link LibraryUtils#pollTracks(Cursor)} to get all results. 417 */ 418 @SuppressWarnings("unchecked") 419 private Cursor getCriteriaCursor( 420 final String columns, 421 final Map<? extends CategoryEnum, ? extends List<? extends String>> criteria, 422 final boolean like, final boolean and, final String orderBy) { 423 final Map<? extends CategoryEnum, ? extends List<? extends String>> crit = criteria != null ? criteria 424 : Collections.EMPTY_MAP; 425 final StringBuilder select = new StringBuilder(); 426 select.append("select distinct "); 427 select.append(columns); 428 select.append(" from tracks"); 429 final boolean isGenre = crit.containsKey(CategoryEnum.Genre) 430 || columns.equals(COLUMN.get(CategoryEnum.Genre)); 431 if (isGenre) { 432 select.append(",tracks_genres,genres"); 433 } 434 if (!crit.isEmpty() || isGenre) { 435 select.append(" where "); 436 } 437 if (isGenre) { 438 select.append("tracks.id=tracks_genres.trackid and " 439 + "genres.id=tracks_genres.genreid "); 440 if (!crit.isEmpty()) 441 select.append("and "); 442 } 443 final List<String> selectionArgs = new ArrayList<String>(); 444 for (final Iterator<? extends CategoryEnum> i = crit.keySet() 445 .iterator(); i.hasNext();) { 446 final CategoryEnum critField = i.next(); 447 final List<? extends String> critValues = crit.get(critField); 448 select.append('('); 449 for (final Iterator<? extends String> j = critValues.iterator(); j 450 .hasNext();) { 451 String critValue = j.next(); 452 select.append(COLUMN.get(critField)); 453 if (critValue == null) { 454 select.append(" isnull"); 455 } else { 456 if (like) { 457 critValue = "%" + critValue + "%"; 458 } 459 if (like) { 460 select.append(" like ?"); 461 } else { 462 select.append("=?"); 463 } 464 selectionArgs.add(critValue); 465 } 466 if (j.hasNext()) { 467 select.append(" or "); 468 } 469 } 470 select.append(')'); 471 if (i.hasNext()) { 472 if (and) { 473 select.append(" and "); 474 } else { 475 select.append(" or "); 476 } 477 } 478 } 479 if (orderBy != null) { 480 select.append(" order by "); 481 select.append(orderBy); 482 } 483 final Cursor result = getDb().rawQuery(select.toString(), 484 selectionArgs.toArray(new String[0])); 485 return result; 486 } 487 488 /*** 489 * Invokes the {@link SQLiteDatabase#rawQuery(String, String[])} method. 490 * 491 * @param select 492 * the SQL SELECT statement. 493 * @param selectionArgs 494 * optional arguments, may be <code>null</code>. 495 * @return opened cursor. 496 */ 497 public Cursor rawQuery(final String select, final String[] selectionArgs) { 498 return getDb().rawQuery(select, selectionArgs); 499 } 500 501 /*** 502 * Loads all genres as a map. 503 * 504 * @return all genres. 505 */ 506 public Map<String, Long> getAllGenres() { 507 final Map<String, Long> result = new HashMap<String, Long>(); 508 final Cursor cursor = getDb().rawQuery("select name,id from genres", 509 null); 510 while (cursor.moveToNext()) { 511 final String genre = cursor.getString(0); 512 final Long id = cursor.getLong(1); 513 result.put(genre, id); 514 } 515 cursor.close(); 516 return result; 517 } 518 }