1 /*
2  * Copyright 2015 The Android Open Source 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 com.example.android.sampletvinput.syncadapter;
18 
19 import android.accounts.Account;
20 import android.content.AbstractThreadedSyncAdapter;
21 import android.content.ContentProviderClient;
22 import android.content.ContentProviderOperation;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.OperationApplicationException;
27 import android.content.SyncResult;
28 import android.media.tv.TvContract;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.RemoteException;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.util.LongSparseArray;
35 
36 import com.example.android.sampletvinput.rich.RichFeedUtil;
37 import com.example.android.sampletvinput.rich.RichTvInputService.ChannelInfo;
38 import com.example.android.sampletvinput.rich.RichTvInputService.ProgramInfo;
39 import com.example.android.sampletvinput.TvContractUtils;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 /**
45  * A SyncAdapter implementation which updates program info periodically.
46  */
47 class SyncAdapter extends AbstractThreadedSyncAdapter {
48     public static final String TAG = "SyncAdapter";
49 
50     public static final String BUNDLE_KEY_INPUT_ID = "bundle_key_input_id";
51     public static final long SYNC_FREQUENCY_SEC = 60 * 60 * 6;  // 6 hours
52     private static final int SYNC_WINDOW_SEC = 60 * 60 * 12;  // 12 hours
53     private static final int BATCH_OPERATION_COUNT = 100;
54 
55     private final Context mContext;
56 
SyncAdapter(Context context, boolean autoInitialize)57     public SyncAdapter(Context context, boolean autoInitialize) {
58         super(context, autoInitialize);
59         mContext = context;
60     }
61 
SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs)62     public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
63         super(context, autoInitialize, allowParallelSyncs);
64         mContext = context;
65     }
66 
67     /**
68      * Called periodically by the system in every {@code SYNC_FREQUENCY_SEC}.
69      */
70     @Override
onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)71     public void onPerformSync(Account account, Bundle extras, String authority,
72             ContentProviderClient provider, SyncResult syncResult) {
73         Log.d(TAG, "onPerformSync(" + account + ", " + authority + ", " + extras + ")");
74         String inputId = extras.getString(SyncAdapter.BUNDLE_KEY_INPUT_ID);
75         if (inputId == null) {
76             return;
77         }
78         List<ChannelInfo> channels = RichFeedUtil.getRichChannels(mContext);
79         LongSparseArray<ChannelInfo> channelMap = TvContractUtils.buildChannelMap(
80                 mContext.getContentResolver(), inputId, channels);
81         for (int i = 0; i < channelMap.size(); ++i) {
82             Uri channelUri = TvContract.buildChannelUri(channelMap.keyAt(i));
83             insertPrograms(channelUri, channelMap.valueAt(i));
84         }
85     }
86 
87     /**
88      * Inserts programs from now to {@link SyncAdapter#SYNC_WINDOW_SEC}.
89      *
90      * @param channelUri The channel where the program info will be added.
91      * @param channelInfo {@link ChannelInfo} instance which includes program information.
92      */
insertPrograms(Uri channelUri, ChannelInfo channelInfo)93     private void insertPrograms(Uri channelUri, ChannelInfo channelInfo) {
94         long durationSumSec = 0;
95         List<ContentValues> programs = new ArrayList<>();
96         for (ProgramInfo program : channelInfo.programs) {
97             durationSumSec += program.durationSec;
98 
99             ContentValues values = new ContentValues();
100             values.put(TvContract.Programs.COLUMN_CHANNEL_ID, ContentUris.parseId(channelUri));
101             values.put(TvContract.Programs.COLUMN_TITLE, program.title);
102             values.put(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.description);
103             values.put(TvContract.Programs.COLUMN_CONTENT_RATING,
104                     TvContractUtils.contentRatingsToString(program.contentRatings));
105             if (!TextUtils.isEmpty(program.posterArtUri)) {
106                 values.put(TvContract.Programs.COLUMN_POSTER_ART_URI, program.posterArtUri);
107             }
108             // NOTE: {@code COLUMN_INTERNAL_PROVIDER_DATA} is a private field where TvInputService
109             // can store anything it wants. Here, we store video type and video URL so that
110             // TvInputService can play the video later with this field.
111             values.put(TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA,
112                     TvContractUtils.convertVideoInfoToInternalProviderData(program.videoType,
113                             program.videoUrl));
114             programs.add(values);
115         }
116 
117         long nowSec = System.currentTimeMillis() / 1000;
118         long insertionEndSec = nowSec + SYNC_WINDOW_SEC;
119         long lastProgramEndTimeSec = TvContractUtils.getLastProgramEndTimeMillis(
120                 mContext.getContentResolver(), channelUri) / 1000;
121         if (nowSec < lastProgramEndTimeSec) {
122             nowSec = lastProgramEndTimeSec;
123         }
124         long insertionStartTimeSec = nowSec - nowSec % durationSumSec;
125         long nextPos = insertionStartTimeSec;
126         for (int i = 0; nextPos < insertionEndSec; ++i) {
127             long programStartSec = nextPos;
128             ArrayList<ContentProviderOperation> ops = new ArrayList<>();
129             int programsCount = channelInfo.programs.size();
130             for (int j = 0; j < programsCount; ++j) {
131                 ProgramInfo program = channelInfo.programs.get(j);
132                 ops.add(ContentProviderOperation.newInsert(
133                         TvContract.Programs.CONTENT_URI)
134                         .withValues(programs.get(j))
135                         .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
136                                 programStartSec * 1000)
137                         .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
138                                 (programStartSec + program.durationSec) * 1000)
139                         .build());
140                 programStartSec = programStartSec + program.durationSec;
141 
142                 // Throttle the batch operation not to face TransactionTooLargeException.
143                 if (j % BATCH_OPERATION_COUNT == BATCH_OPERATION_COUNT - 1
144                         || j == programsCount - 1) {
145                     try {
146                         mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
147                     } catch (RemoteException | OperationApplicationException e) {
148                         Log.e(TAG, "Failed to insert programs.", e);
149                         return;
150                     }
151                     ops.clear();
152                 }
153             }
154             nextPos = insertionStartTimeSec + i * durationSumSec;
155         }
156     }
157 }
158