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