Search code examples
androidcursorandroid-sqliteandroid-room

Room: replace SQLite cursor in Dao?


I have a JobIntentService that re-sets pending Alarm Notifications from an SQLite database. It uses a query and a Cursor to get the Notification dates from 4 different columns in the database. I am converting to a Room database and not sure how to convert the cursor to a Dao method. Do I need to use @Transaction since I am getting Notifications from multiple columns in the database? Would appreciate any insights or thoughts on how to build in Room.

Service

public class Service extends JobIntentService {

static final int JOB_ID = 9;

public static void enqueueWork(Context context, Intent work) {
    enqueueWork(context, RebootService.class, JOB_ID, work);
} 

@Override
protected void onHandleWork(@NonNull Intent intent) {

    AlarmManager alarmManager1;
    Intent brIntent1;
    PendingIntent pendingIntent1;

    SQLiteDB sqLiteDB = SQLiteDB.getInstance(this);
    Calendar cal1 = Calendar.getInstance();

    Cursor cursor = sqLiteDB.resetNotifications(); 

     try {
          if (cursor.getCount() > 0) { 
              cursor.moveToFirst(); 

              int dueDatentimeColumnIndex = cursor.getColumnIndex(ItemContract.ItemEntry.COLUMN_DUEDATENTIME);
              int notifColumnIndex1 = cursor.getColumnIndex(ItemContract.ItemEntry.COLUMN_NOTIFTIME);
              int notif2ColumnIndex2 = cursor.getColumnIndex(ItemContract.ItemEntry.COLUMN_NOTIFTIME2);
              int randColumnIndex1 = cursor.getColumnIndex(ItemContract.ItemEntry.COLUMN_RANDINT);

              while (!cursor.isAfterLast()) {  

                  do {

                      long notifTime1 = cursor.getLong(notifColumnIndex1);
                      int randInt1 = cursor.getInt(randColumnIndex1);
                      cal1.setTime(new Date(notifTime1));

                      // Set up a system AlarmManager to fire a future alarm that sends a Notification
                      // even if the app is in the background or closed.
                      alarmManager1 = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

                      if (cal1.getTimeInMillis() > System.currentTimeMillis()) {                                            
                          brIntent1 = new Intent(this, AlarmReceiver.class);
                       brIntent1.setAction("24Hour");

                       pendingIntent1 = PendingIntent.getBroadcast(this, randInt1, brIntent1,
                                    PendingIntent.FLAG_ONE_SHOT);

                      if (alarmManager1 != null && notifTime1 != -1) {
                          alarmManager1.set(AlarmManager.RTC_WAKEUP, cal1.getTimeInMillis(), pendingIntent1);
                      }
...       
}

SQLiteDB.java

...
public Cursor resetNotifications() {

   SQLiteDatabase db = getReadableDatabase();

   String[] columns = new String[]{
                ItemContract.ItemEntry.COLUMN_NOTIFTIME,
                ItemContract.ItemEntry.COLUMN_NOTIFTIME2,
                ItemContract.ItemEntry.COLUMN_DUEDATENTIME,
                ItemContract.ItemEntry.COLUMN_RANDINT};

        return db.query(
                TABLE_NAME, 
                columns, // The columns to return
                null,      
                null,   
                null,      
                null,       
                null       
        ); 
}

This is the code I came up as a replacement:

public class RebootService extends JobIntentService {

// Unique job ID for this service
static final int JOB_ID = 10000;

// Convenience method for enqueueing work to this service.
public static void enqueueWork(Context context, Intent work) {
    enqueueWork(context, RebootService.class, JOB_ID, work);
}

private QuickcardRepository reposit1;

@Override
protected void onHandleWork(@NonNull Intent intent) {

    reposit1 = new QuickcardRepository(getApplication());

    Bundle extras = intent.getExtras(); // Returns the Intent *that started this Service.*
    if (extras != null) {

        String classname = extras.getString("TAG");

        if (classname != null && classname.equals("bootCompleted")) {

            AlarmManager alarmManager1;
            Intent brIntent1, brIntent2, brIntent3;
            PendingIntent pendingIntent1, pendingIntent2, pendingIntent3;

            Calendar cal1 = Calendar.getInstance();
            Calendar cal2 = Calendar.getInstance();
            Calendar cal3 = Calendar.getInstance();

            List<Quickcard> resetNotificationsList = reposit1.getNotifications();
            // Cycle through the Room database rows to get the Notifications data
            for (Quickcard quickcard: resetNotificationsList) {
                 // Quickcards without a Due date get a Due date in the database of -1.
                 // Therefore, only cycle through and get data for those quickcards that have
                //  a Due data and therefore have Notifications (reminders) where the actual
                //  "Due date" is != -1.
                 if(quickcard.getDuedatentime() != -1) {

                     // Set up the 24-Hour calendar object.
                     long notifTime1 = quickcard.getNotiftime();
                     int randInt1 = quickcard.getRandint();
                     cal1.setTime(new Date(notifTime1));

                     // Set up a system AlarmManager to fire a future alarm that sends a Notification
                     // even if the app is in the background or closed.  Have to add "context" here.
                     alarmManager1 = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

                     // If the stated alarm trigger time is in the past, the alarm will be triggered immediately.
                     // So only set Alarms to fire Notifications for Duedates in the future
                     // (meaning > than the current System time).  Ignore those in the past.
                     if (cal1.getTimeInMillis() > System.currentTimeMillis()) {
                         // For the 24Hour Notifications.
                         // Set up a PendingIntent that will perform broadcast to the BroadcastReceiver.
                         brIntent1 = new Intent(this, AlarmReceiver.class);
                         brIntent1.setAction("24Hour");
                         // Need to use FLAG_ONE_SHOT on the PendingIntent, not FLAG_UPDATE_CURRENT.
                         // A random int is used to be able to set multiple alarms and then to be able to
                         // delete them later (if the user for ex., deletes the quickCards Duedate) using
                         // the same random int.
                         pendingIntent1 = PendingIntent.getBroadcast(this, randInt1, brIntent1,
                                 PendingIntent.FLAG_ONE_SHOT);

                         // Alarms have 3 properties below after "set(...)":
                         // 1) Alarm type:  RTC_WAKEUP type is chosen here to wake the device from sleep.
                         // 2) Trigger time: in this case, 24 hours before the Duedate/Duetime is reached.
                         // 3) Pending Intent:  A future Intent that is sent when the trigger time is reached.
                         int SDK_INT1 = Build.VERSION.SDK_INT;
                         if (SDK_INT1 >= Build.VERSION_CODES.M) {
                             // Wakes up the device in Doze Mode for API Level 23 and higher.
                             // The "... != -1" test only sets up pendingIntents for quickCards that have
                             // a Notification.  quickCards with no Duedate & Duetime are bypassed.
                             if (alarmManager1 != null && notifTime1 != -1) {
                                 alarmManager1.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, cal1.getTimeInMillis(),
                                         pendingIntent1);
                             }
                         } else if (SDK_INT1 >= Build.VERSION_CODES.KITKAT) {
                             // Wakes up the device in Idle Mode for API Level 19 to 22.
                             // The "... != -1" test only sets up pendingIntents for quickCards that have
                             // a Notification.  quickCards with no Duedate & Duetime are bypassed.
                             if (alarmManager1 != null && notifTime1 != -1) {
                                 alarmManager1.setExact(AlarmManager.RTC_WAKEUP, cal1.getTimeInMillis(), pendingIntent1);
                             }
                         } else {
                             // Old APIs Level 18 and below.
                             // The "... != -1" test only sets up pendingIntents for quickCards that have
                             // a Notification.  quickCards with no Duedate & Duetime are bypassed.
                             if (alarmManager1 != null && notifTime1 != -1) {
                                 alarmManager1.set(AlarmManager.RTC_WAKEUP, cal1.getTimeInMillis(), pendingIntent1);
                             }
                         }
                     }

Solution

  • I believe that @Transaction wraps the code in a transaction. This is already done for all but @Query's (if the @Query isn't an update/delete query (if it is an update or delete query, it is wrapped within a transaction)).

    I believe the issue as to whether or not a SELECT query should be wrapped within a transaction (@Transaction @Query......) is if @Relation is used. If it is then the List(s) of the related/associated items/objects are run as separate queries and thus running them all in a transaction will ensure consistent data. Without there is the potential that underlying data could be changed by other transactions and thus the resultant data could be inconsistent.

    Saying that it would hardly have an impact to use @Transaction where it is not needed and may even have a positive impact if it were coded where it might inadvertently not bee coded.

    Of course you can always return a Cursor anyway using Room. You may wish to have a look at Associating tables using Room database in Android Studio, which has some examples.

    Based upon your code, primarily that you have an ItemContract with a subclass of ItemEntry then the Entity for an ItemEntry could be in ItemEntry.java, as per :-

    @Entity
    public class ItemEntry {
    
        @PrimaryKey(autoGenerate = true)
        private long id;
        @ColumnInfo(name = COLUMN_NOTIFTIME)
        private long notiftime;
        @ColumnInfo(name = COLUMN_NOTIFTIME2)
        private long notiftime2;
        @ColumnInfo(name = COLUMN_DUEDATENTIME)
        private long duedatentime;
        @ColumnInfo(name = COLUMN_RANDINT)
        public int randint;
    
        public ItemEntry(){
    
        }
    
        @Ignore
        public ItemEntry(long notiftime, long notiftime2, long duedatentime, int randint) {
            this.notiftime = notiftime;
            this.notiftime2 = notiftime2;
            this.duedatentime = duedatentime;
            this.randint = randint;
        }
    
        public long getId() {
            return id;
        }
    
        public void setId(long id) {
            this.id = id;
        }
    
        public long getNotiftime() {
            return notiftime;
        }
    
        public void setNotiftime(long notiftime) {
            this.notiftime = notiftime;
        }
    
        public long getNotiftime2() {
            return notiftime2;
        }
    
        public void setNotiftime2(long notiftime2) {
            this.notiftime2 = notiftime2;
        }
    
        public long getDuedatentime() {
            return duedatentime;
        }
    
        public void setDuedatentime(long duedatentime) {
            this.duedatentime = duedatentime;
        }
    
        public int getRandint() {
            return randint;
        }
    
        public void setRandint(int randint) {
            this.randint = randint;
        }
    } 
    

    Along with an interface ItemEntryDao.java as :-

    @Dao
    interface ItemEntryDao {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        long[] insertItemEntries(ItemEntry... itemEntries);
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        long insertItemEntry(ItemEntry itemEntry);
        @Update(onConflict = OnConflictStrategy.IGNORE)
        int updateItemEnrties(ItemEntry... itemEntries);
        @Update(onConflict = OnConflictStrategy.IGNORE)
        int updateItemEntry(ItemEntry itemEntry);
        @Delete
        int deleteItemEntries(ItemEntry... itemEntries);
        @Delete
        int deleteItemEntry(ItemEntry itemEntry);
        @Query("SELECT * FROM ItemEntry")
        List<ItemEntry> resetNotifications();
    }
    
    • The @Query is the equivalent to the Cursor BUT returns a List of ItemEntry objects.

    The above could be used for example (that very basically replicates your code, but outputs the extracted data to the log) like :-

    public void onHandleWork() {
    
        ItemEntry ie = new ItemEntry();
        ie.setNotiftime(100);
        ie.setNotiftime2(200);
        ie.setDuedatentime(500000);
        ie.setRandint(567);
        mDB.getItemEntryDao().insertItemEntry(ie);
        List<ItemEntry> mylist = mDB.getItemEntryDao().resetNotifications();
        for (ItemEntry itementry: mylist) {
            Log.d("ITEMENTRY",
                    "\n\tnotiftime= " + String.valueOf(itementry.getNotiftime()) +
                            "\n\tnotiftime2= " + String.valueOf(itementry.getNotiftime2()) +
                            "\n\tduedatetime= " + String.valueOf(itementry.getDuedatentime()) +
                            "\n\trandint= " + String.valueOf(itementry.getRandint())
    
            );
        }
    }
    
    • mDB is the built object (i.e. an instance of the @Database class)

    This would result in (for the first run) :-

    05-28 14:31:14.587 7211-7211/aso.so56326640 D/ITEMENTRY:  notiftime= 100
          notiftime2= 200
          duedatetime= 500000
          randint= 567