OSDN Git Service

5fcd83f68bafb560ff369b85daccb0bf83cca662
[android-x86/packages-apps-Eleven.git] / src / org / lineageos / eleven / provider / SongPlayCount.java
1 /*
2 * Copyright (C) 2014 The CyanogenMod Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.lineageos.eleven.provider;
18
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.view.animation.AccelerateInterpolator;
24 import android.view.animation.Interpolator;
25
26 import java.util.HashSet;
27 import java.util.Iterator;
28
29 /**
30  * This database tracks the number of play counts for an individual song.  This is used to drive
31  * the top played tracks as well as the playlist images
32  */
33 public class SongPlayCount {
34     private static SongPlayCount sInstance = null;
35
36     private MusicDB mMusicDatabase = null;
37
38     // interpolator curve applied for measuring the curve
39     private static Interpolator sInterpolator = new AccelerateInterpolator(1.5f);
40
41     // how many weeks worth of playback to track
42     private static final int NUM_WEEKS = 52;
43
44     // how high to multiply the interpolation curve
45     private static int INTERPOLATOR_HEIGHT = 50;
46
47     // how high the base value is. The ratio of the Height to Base is what really matters
48     private static int INTERPOLATOR_BASE = 25;
49
50     private static int ONE_WEEK_IN_MS = 1000 * 60 * 60 * 24 * 7;
51
52     private static String WHERE_ID_EQUALS = SongPlayCountColumns.ID + "=?";
53
54     // number of weeks since epoch time
55     private int mNumberOfWeeksSinceEpoch;
56
57     // used to track if we've walkd through the db and updated all the rows
58     private boolean mDatabaseUpdated;
59
60     /**
61      * Constructor of <code>RecentStore</code>
62      *
63      * @param context The {@link android.content.Context} to use
64      */
65     public SongPlayCount(final Context context) {
66         mMusicDatabase = MusicDB.getInstance(context);
67
68         long msSinceEpoch = System.currentTimeMillis();
69         mNumberOfWeeksSinceEpoch = (int)(msSinceEpoch / ONE_WEEK_IN_MS);
70
71         mDatabaseUpdated = false;
72     }
73
74     public void onCreate(final SQLiteDatabase db) {
75         // create the play count table
76         // WARNING: If you change the order of these columns
77         // please update getColumnIndexForWeek
78         StringBuilder builder = new StringBuilder();
79         builder.append("CREATE TABLE IF NOT EXISTS ");
80         builder.append(SongPlayCountColumns.NAME);
81         builder.append("(");
82         builder.append(SongPlayCountColumns.ID);
83         builder.append(" INT UNIQUE,");
84
85         for (int i = 0; i < NUM_WEEKS; i++) {
86             builder.append(getColumnNameForWeek(i));
87             builder.append(" INT DEFAULT 0,");
88         }
89
90         builder.append(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX);
91         builder.append(" INT NOT NULL,");
92
93         builder.append(SongPlayCountColumns.PLAYCOUNTSCORE);
94         builder.append(" REAL DEFAULT 0);");
95
96         db.execSQL(builder.toString());
97     }
98
99     public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
100         // No upgrade path needed yet
101     }
102
103     public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
104         // If we ever have downgrade, drop the table to be safe
105         db.execSQL("DROP TABLE IF EXISTS " + SongPlayCountColumns.NAME);
106         onCreate(db);
107     }
108
109     /**
110      * @param context The {@link android.content.Context} to use
111      * @return A new instance of this class.
112      */
113     public static final synchronized SongPlayCount getInstance(final Context context) {
114         if (sInstance == null) {
115             sInstance = new SongPlayCount(context.getApplicationContext());
116         }
117         return sInstance;
118     }
119
120     /**
121      * Increases the play count of a song by 1
122      * @param songId The song id to increase the play count
123      */
124     public void bumpSongCount(final long songId) {
125         if (songId < 0) {
126             return;
127         }
128
129         final SQLiteDatabase database = mMusicDatabase.getWritableDatabase();
130         updateExistingRow(database, songId, true);
131     }
132
133     /**
134      * This creates a new entry that indicates a song has been played once as well as its score
135      * @param database a writeable database
136      * @param songId the id of the track
137      */
138     private void createNewPlayedEntry(final SQLiteDatabase database, final long songId) {
139         // no row exists, create a new one
140         float newScore = getScoreMultiplierForWeek(0);
141         int newPlayCount = 1;
142
143         final ContentValues values = new ContentValues(3);
144         values.put(SongPlayCountColumns.ID, songId);
145         values.put(SongPlayCountColumns.PLAYCOUNTSCORE, newScore);
146         values.put(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX, mNumberOfWeeksSinceEpoch);
147         values.put(getColumnNameForWeek(0), newPlayCount);
148
149         database.insert(SongPlayCountColumns.NAME, null, values);
150     }
151
152     /**
153      * This function will take a song entry and update it to the latest week and increase the count
154      * for the current week by 1 if necessary
155      * @param database a writeable database
156      * @param id the id of the track to bump
157      * @param bumpCount whether to bump the current's week play count by 1 and adjust the score
158      */
159     private void updateExistingRow(final SQLiteDatabase database, final long id, boolean bumpCount) {
160         String stringId = String.valueOf(id);
161
162         // begin the transaction
163         database.beginTransaction();
164
165         // get the cursor of this content inside the transaction
166         final Cursor cursor = database.query(SongPlayCountColumns.NAME, null, WHERE_ID_EQUALS,
167                 new String[] { stringId }, null, null, null);
168
169         // if we have a result
170         if (cursor != null && cursor.moveToFirst()) {
171             // figure how many weeks since we last updated
172             int lastUpdatedIndex = cursor.getColumnIndex(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX);
173             int lastUpdatedWeek = cursor.getInt(lastUpdatedIndex);
174             int weekDiff = mNumberOfWeeksSinceEpoch - lastUpdatedWeek;
175
176             // if it's more than the number of weeks we track, delete it and create a new entry
177             if (Math.abs(weekDiff) >= NUM_WEEKS) {
178                 // this entry needs to be dropped since it is too outdated
179                 deleteEntry(database, stringId);
180                 if (bumpCount) {
181                     createNewPlayedEntry(database, id);
182                 }
183             } else if (weekDiff != 0) {
184                 // else, shift the weeks
185                 int[] playCounts = new int[NUM_WEEKS];
186
187                 if (weekDiff > 0) {
188                     // time is shifted forwards
189                     for (int i = 0; i < NUM_WEEKS - weekDiff; i++) {
190                         playCounts[i + weekDiff] = cursor.getInt(getColumnIndexForWeek(i));
191                     }
192                 } else if (weekDiff < 0) {
193                     // time is shifted backwards (by user) - nor typical behavior but we
194                     // will still handle it
195
196                     // since weekDiff is -ve, NUM_WEEKS + weekDiff is the real # of weeks we have to
197                     // transfer.  Then we transfer the old week i - weekDiff to week i
198                     // for example if the user shifted back 2 weeks, ie -2, then for 0 to
199                     // NUM_WEEKS + (-2) we set the new week i = old week i - (-2) or i+2
200                     for (int i = 0; i < NUM_WEEKS + weekDiff; i++) {
201                         playCounts[i] = cursor.getInt(getColumnIndexForWeek(i - weekDiff));
202                     }
203                 }
204
205                 // bump the count
206                 if (bumpCount) {
207                     playCounts[0]++;
208                 }
209
210                 float score = calculateScore(playCounts);
211
212                 // if the score is non-existant, then delete it
213                 if (score < .01f) {
214                     deleteEntry(database, stringId);
215                 } else {
216                     // create the content values
217                     ContentValues values = new ContentValues(NUM_WEEKS + 2);
218                     values.put(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX, mNumberOfWeeksSinceEpoch);
219                     values.put(SongPlayCountColumns.PLAYCOUNTSCORE, score);
220
221                     for (int i = 0; i < NUM_WEEKS; i++) {
222                         values.put(getColumnNameForWeek(i), playCounts[i]);
223                     }
224
225                     // update the entry
226                     database.update(SongPlayCountColumns.NAME, values, WHERE_ID_EQUALS,
227                             new String[]{stringId});
228                 }
229             } else if (bumpCount) {
230                 // else no shifting, just update the scores
231                 ContentValues values = new ContentValues(2);
232
233                 // increase the score by a single score amount
234                 int scoreIndex = cursor.getColumnIndex(SongPlayCountColumns.PLAYCOUNTSCORE);
235                 float score = cursor.getFloat(scoreIndex) + getScoreMultiplierForWeek(0);
236                 values.put(SongPlayCountColumns.PLAYCOUNTSCORE, score);
237
238                 // increase the play count by 1
239                 values.put(getColumnNameForWeek(0), cursor.getInt(getColumnIndexForWeek(0)) + 1);
240
241                 // update the entry
242                 database.update(SongPlayCountColumns.NAME, values, WHERE_ID_EQUALS,
243                         new String[]{stringId});
244             }
245
246             cursor.close();
247         } else if (bumpCount) {
248             // if we have no existing results, create a new one
249             createNewPlayedEntry(database, id);
250         }
251
252         database.setTransactionSuccessful();
253         database.endTransaction();
254     }
255
256     public void deleteAll() {
257         final SQLiteDatabase database = mMusicDatabase.getWritableDatabase();
258         database.delete(SongPlayCountColumns.NAME, null, null);
259     }
260
261     /**
262      * Gets a cursor containing the top songs played.  Note this only returns songs that have been
263      * played at least once in the past NUM_WEEKS
264      * @param numResults number of results to limit by.  If <= 0 it returns all results
265      * @return the top tracks
266      */
267     public Cursor getTopPlayedResults(int numResults) {
268         updateResults();
269
270         final SQLiteDatabase database = mMusicDatabase.getReadableDatabase();
271         return database.query(SongPlayCountColumns.NAME, new String[] { SongPlayCountColumns.ID },
272                 null, null, null, null, SongPlayCountColumns.PLAYCOUNTSCORE + " DESC",
273                 (numResults <= 0 ? null : String.valueOf(numResults)));
274     }
275
276     /**
277      * Given a list of ids, it sorts the results based on the most played results
278      * @param ids list
279      * @return sorted list - this may be smaller than the list passed in for performance reasons
280      */
281     public long[] getTopPlayedResultsForList(long[] ids) {
282         final int MAX_NUMBER_SONGS_TO_ANALYZE = 250;
283
284         if (ids == null || ids.length == 0) {
285             return null;
286         }
287
288         HashSet<Long> uniqueIds = new HashSet<Long>(ids.length);
289
290         // create the list of ids to select against
291         StringBuilder selection = new StringBuilder();
292         selection.append(SongPlayCountColumns.ID);
293         selection.append(" IN (");
294
295         // add the first element to handle the separator case for the first element
296         uniqueIds.add(ids[0]);
297         selection.append(ids[0]);
298
299         for (int i = 1; i < ids.length; i++) {
300             // if the new id doesn't exist
301             if (uniqueIds.add(ids[i])) {
302                 // append a separator
303                 selection.append(",");
304
305                 // append the id
306                 selection.append(ids[i]);
307
308                 // for performance reasons, only look at a certain number of songs
309                 // in case their playlist is ridiculously large
310                 if (uniqueIds.size() >= MAX_NUMBER_SONGS_TO_ANALYZE) {
311                     break;
312                 }
313             }
314         }
315
316         // close out the selection
317         selection.append(")");
318
319         long[] sortedList = new long[uniqueIds.size()];
320
321         // now query for the songs
322         final SQLiteDatabase database = mMusicDatabase.getReadableDatabase();
323         Cursor topSongsCursor = null;
324         int idx = 0;
325
326         try {
327             topSongsCursor = database.query(SongPlayCountColumns.NAME,
328                     new String[]{ SongPlayCountColumns.ID }, selection.toString(), null, null,
329                     null, SongPlayCountColumns.PLAYCOUNTSCORE + " DESC");
330
331             if (topSongsCursor != null && topSongsCursor.moveToFirst()) {
332                 do {
333                     // for each id found, add it to the list and remove it from the unique ids
334                     long id = topSongsCursor.getLong(0);
335                     sortedList[idx++] = id;
336                     uniqueIds.remove(id);
337                 } while (topSongsCursor.moveToNext());
338             }
339         } finally {
340             if (topSongsCursor != null) {
341                 topSongsCursor.close();
342                 topSongsCursor = null;
343             }
344         }
345
346         // append the remaining items - these are songs that haven't been played recently
347         Iterator<Long> iter = uniqueIds.iterator();
348         while (iter.hasNext()) {
349             sortedList[idx++] = iter.next();
350         }
351
352         return sortedList;
353     }
354
355     /**
356      * This updates all the results for the getTopPlayedResults so that we can get an
357      * accurate list of the top played results
358      */
359     private synchronized void updateResults() {
360         if (mDatabaseUpdated) {
361             return;
362         }
363
364         final SQLiteDatabase database = mMusicDatabase.getWritableDatabase();
365
366         database.beginTransaction();
367
368         int oldestWeekWeCareAbout = mNumberOfWeeksSinceEpoch - NUM_WEEKS + 1;
369         // delete rows we don't care about anymore
370         database.delete(SongPlayCountColumns.NAME, SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX
371                 + " < " + oldestWeekWeCareAbout, null);
372
373         // get the remaining rows
374         Cursor cursor = database.query(SongPlayCountColumns.NAME,
375                 new String[] { SongPlayCountColumns.ID },
376                 null, null, null, null, null);
377
378         if (cursor != null && cursor.moveToFirst()) {
379             // for each row, update it
380             do {
381                 updateExistingRow(database, cursor.getLong(0), false);
382             } while (cursor.moveToNext());
383
384             cursor.close();
385             cursor = null;
386         }
387
388         mDatabaseUpdated = true;
389         database.setTransactionSuccessful();
390         database.endTransaction();
391     }
392
393     /**
394      * @param songId The song Id to remove.
395      */
396     public void removeItem(final long songId) {
397         final SQLiteDatabase database = mMusicDatabase.getWritableDatabase();
398         deleteEntry(database, String.valueOf(songId));
399     }
400
401     /**
402      * Deletes the entry
403      * @param database database to use
404      * @param stringId id to delete
405      */
406     private void deleteEntry(final SQLiteDatabase database, final String stringId) {
407         database.delete(SongPlayCountColumns.NAME, WHERE_ID_EQUALS, new String[]{stringId});
408     }
409
410     /**
411      * Calculates the score of the song given the play counts
412      * @param playCounts an array of the # of times a song has been played for each week
413      *                   where playCounts[N] is the # of times it was played N weeks ago
414      * @return the score
415      */
416     private static float calculateScore(final int[] playCounts) {
417         if (playCounts == null) {
418             return 0;
419         }
420
421         float score = 0;
422         for (int i = 0; i < Math.min(playCounts.length, NUM_WEEKS); i++) {
423             score += playCounts[i] * getScoreMultiplierForWeek(i);
424         }
425
426         return score;
427     }
428
429     /**
430      * Gets the column name for each week #
431      * @param week number
432      * @return the column name
433      */
434     private static String getColumnNameForWeek(final int week) {
435         return SongPlayCountColumns.WEEK_PLAY_COUNT + String.valueOf(week);
436     }
437
438     /**
439      * Gets the score multiplier for each week
440      * @param week number
441      * @return the multiplier to apply
442      */
443     private static float getScoreMultiplierForWeek(final int week) {
444         return sInterpolator.getInterpolation(1 - (week / (float)NUM_WEEKS)) * INTERPOLATOR_HEIGHT
445                 + INTERPOLATOR_BASE;
446     }
447
448     /**
449      * For some performance gain, return a static value for the column index for a week
450      * WARNIGN: This function assumes you have selected all columns for it to work
451      * @param week number
452      * @return column index of that week
453      */
454     private static int getColumnIndexForWeek(final int week) {
455         // ID, followed by the weeks columns
456         return 1 + week;
457     }
458
459     public interface SongPlayCountColumns {
460
461         /* Table name */
462         public static final String NAME = "songplaycount";
463
464         /* Song IDs column */
465         public static final String ID = "songid";
466
467         /* Week Play Count */
468         public static final String WEEK_PLAY_COUNT = "week";
469
470         /* Weeks since Epoch */
471         public static final String LAST_UPDATED_WEEK_INDEX = "weekindex";
472
473         /* Play count */
474         public static final String PLAYCOUNTSCORE = "playcountscore";
475     }
476 }