1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package com.example.android.tv.channelsprograms; 15 16 import android.app.job.JobParameters; 17 import android.app.job.JobService; 18 import android.content.ContentUris; 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.net.Uri; 22 import android.os.AsyncTask; 23 import android.os.PersistableBundle; 24 import android.support.annotation.NonNull; 25 import android.support.media.tv.Channel; 26 import android.support.media.tv.PreviewProgram; 27 import android.support.media.tv.TvContractCompat; 28 import android.util.Log; 29 30 import com.example.android.tv.channelsprograms.model.MockDatabase; 31 import com.example.android.tv.channelsprograms.model.MockMovieService; 32 import com.example.android.tv.channelsprograms.model.Movie; 33 import com.example.android.tv.channelsprograms.model.Subscription; 34 import com.example.android.tv.channelsprograms.util.AppLinkHelper; 35 import com.example.android.tv.channelsprograms.util.TvUtil; 36 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.List; 40 41 /** 42 * Syncs programs for a channel. A channel id is required to be passed via the {@link 43 * JobParameters}. This service is scheduled to listen to changes to a channel. Once the job 44 * completes, it will reschedule itself to listen for the next change to the channel. See {@link 45 * TvUtil#scheduleSyncingProgramsForChannel(Context, long)} for more details about the scheduling. 46 */ 47 public class SyncProgramsJobService extends JobService { 48 49 private static final String TAG = "SyncProgramsJobService"; 50 51 private SyncProgramsTask mSyncProgramsTask; 52 53 @Override 54 public boolean onStartJob(final JobParameters jobParameters) { 55 Log.d(TAG, "onStartJob(): " + jobParameters); 56 57 final long channelId = getChannelId(jobParameters); 58 if (channelId == -1L) { 59 return false; 60 } 61 Log.d(TAG, "onStartJob(): Scheduling syncing for programs for channel " + channelId); 62 63 mSyncProgramsTask = 64 new SyncProgramsTask(getApplicationContext()) { 65 @Override 66 protected void onPostExecute(Boolean finished) { 67 super.onPostExecute(finished); 68 // Daisy chain listening for the next change to the channel. 69 TvUtil.scheduleSyncingProgramsForChannel( 70 SyncProgramsJobService.this, channelId); 71 mSyncProgramsTask = null; 72 jobFinished(jobParameters, !finished); 73 } 74 }; 75 mSyncProgramsTask.execute(channelId); 76 77 return true; 78 } 79 80 @Override 81 public boolean onStopJob(JobParameters jobParameters) { 82 if (mSyncProgramsTask != null) { 83 mSyncProgramsTask.cancel(true); 84 } 85 return true; 86 } 87 88 private long getChannelId(JobParameters jobParameters) { 89 PersistableBundle extras = jobParameters.getExtras(); 90 if (extras == null) { 91 return -1L; 92 } 93 94 return extras.getLong(TvContractCompat.EXTRA_CHANNEL_ID, -1L); 95 } 96 97 /* 98 * Syncs programs by querying the given channel id. 99 * 100 * If the channel is not browsable, the programs will be removed to avoid showing 101 * stale programs when the channel becomes browsable in the future. 102 * 103 * If the channel is browsable, then it will check if the channel has any programs. 104 * If the channel does not have any programs, new programs will be added. 105 * If the channel does have programs, then a fresh list of programs will be fetched and the 106 * channel's programs will be updated. 107 */ 108 private void syncPrograms(long channelId, List<Movie> initialMovies) { 109 Log.d(TAG, "Sync programs for channel: " + channelId); 110 List<Movie> movies = new ArrayList<>(initialMovies); 111 112 try (Cursor cursor = 113 getContentResolver() 114 .query( 115 TvContractCompat.buildChannelUri(channelId), 116 null, 117 null, 118 null, 119 null)) { 120 if (cursor != null && cursor.moveToNext()) { 121 Channel channel = Channel.fromCursor(cursor); 122 if (!channel.isBrowsable()) { 123 Log.d(TAG, "Channel is not browsable: " + channelId); 124 deletePrograms(channelId, movies); 125 } else { 126 Log.d(TAG, "Channel is browsable: " + channelId); 127 if (movies.isEmpty()) { 128 movies = createPrograms(channelId, MockMovieService.getList()); 129 } else { 130 movies = updatePrograms(channelId, movies); 131 } 132 MockDatabase.saveMovies(getApplicationContext(), channelId, movies); 133 } 134 } 135 } 136 } 137 138 private List<Movie> createPrograms(long channelId, List<Movie> movies) { 139 140 List<Movie> moviesAdded = new ArrayList<>(movies.size()); 141 for (Movie movie : movies) { 142 PreviewProgram previewProgram = buildProgram(channelId, movie); 143 144 Uri programUri = 145 getContentResolver() 146 .insert( 147 TvContractCompat.PreviewPrograms.CONTENT_URI, 148 previewProgram.toContentValues()); 149 long programId = ContentUris.parseId(programUri); 150 Log.d(TAG, "Inserted new program: " + programId); 151 movie.setProgramId(programId); 152 moviesAdded.add(movie); 153 } 154 155 return moviesAdded; 156 } 157 158 private List<Movie> updatePrograms(long channelId, List<Movie> movies) { 159 160 // By getting a fresh list, we should see a visible change in the home screen. 161 List<Movie> updateMovies = MockMovieService.getFreshList(); 162 for (int i = 0; i < movies.size(); ++i) { 163 Movie old = movies.get(i); 164 Movie update = updateMovies.get(i); 165 long programId = old.getProgramId(); 166 167 getContentResolver() 168 .update( 169 TvContractCompat.buildPreviewProgramUri(programId), 170 buildProgram(channelId, update).toContentValues(), 171 null, 172 null); 173 Log.d(TAG, "Updated program: " + programId); 174 update.setProgramId(programId); 175 } 176 177 return updateMovies; 178 } 179 180 private void deletePrograms(long channelId, List<Movie> movies) { 181 if (movies.isEmpty()) { 182 return; 183 } 184 185 int count = 0; 186 for (Movie movie : movies) { 187 count += 188 getContentResolver() 189 .delete( 190 TvContractCompat.buildPreviewProgramUri(movie.getProgramId()), 191 null, 192 null); 193 } 194 Log.d(TAG, "Deleted " + count + " programs for channel " + channelId); 195 196 // Remove our local records to stay in sync with the TV Provider. 197 MockDatabase.removeMovies(getApplicationContext(), channelId); 198 } 199 200 @NonNull 201 private PreviewProgram buildProgram(long channelId, Movie movie) { 202 Uri posterArtUri = Uri.parse(movie.getCardImageUrl()); 203 Uri appLinkUri = AppLinkHelper.buildPlaybackUri(channelId, movie.getId()); 204 Uri previewVideoUri = Uri.parse(movie.getVideoUrl()); 205 206 PreviewProgram.Builder builder = new PreviewProgram.Builder(); 207 builder.setChannelId(channelId) 208 .setType(TvContractCompat.PreviewProgramColumns.TYPE_CLIP) 209 .setTitle(movie.getTitle()) 210 .setDescription(movie.getDescription()) 211 .setPosterArtUri(posterArtUri) 212 .setPreviewVideoUri(previewVideoUri) 213 .setIntentUri(appLinkUri); 214 return builder.build(); 215 } 216 217 private class SyncProgramsTask extends AsyncTask<Long, Void, Boolean> { 218 219 private final Context mContext; 220 221 private SyncProgramsTask(Context context) { 222 this.mContext = context; 223 } 224 225 @Override 226 protected Boolean doInBackground(Long... channelIds) { 227 List<Long> params = Arrays.asList(channelIds); 228 if (!params.isEmpty()) { 229 for (Long channelId : params) { 230 Subscription subscription = 231 MockDatabase.findSubscriptionByChannelId(mContext, channelId); 232 if (subscription != null) { 233 List<Movie> cachedMovies = MockDatabase.getMovies(mContext, channelId); 234 syncPrograms(channelId, cachedMovies); 235 } 236 } 237 } 238 return true; 239 } 240 } 241 } 242