Home | History | Annotate | Download | only in channelsprograms
      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