1 /* 2 * Copyright (C) 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.android.tv.data; 18 19 import android.content.ContentProviderOperation; 20 import android.content.Context; 21 import android.content.OperationApplicationException; 22 import android.content.SharedPreferences; 23 import android.graphics.Bitmap.CompressFormat; 24 import android.media.tv.TvContract; 25 import android.net.Uri; 26 import android.os.AsyncTask; 27 import android.os.RemoteException; 28 import android.support.annotation.MainThread; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import com.android.tv.common.util.PermissionUtils; 32 import com.android.tv.common.util.SharedPreferencesUtils; 33 import com.android.tv.data.api.Channel; 34 import com.android.tv.util.images.BitmapUtils; 35 import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo; 36 import java.io.IOException; 37 import java.io.OutputStream; 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Map; 41 42 /** 43 * Fetches channel logos from the cloud into the database. It's for the channels which have no logos 44 * or need update logos. This class is thread safe. 45 */ 46 public class ChannelLogoFetcher { 47 private static final String TAG = "ChannelLogoFetcher"; 48 private static final boolean DEBUG = false; 49 50 private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO = 51 "is_first_time_fetch_channel_logo"; 52 53 private static FetchLogoTask sFetchTask; 54 55 /** 56 * Fetches the channel logos from the cloud data and insert them into TvProvider. The previous 57 * task is canceled and a new task starts. 58 */ 59 @MainThread startFetchingChannelLogos(Context context, List<Channel> channels)60 public static void startFetchingChannelLogos(Context context, List<Channel> channels) { 61 if (!PermissionUtils.hasAccessAllEpg(context)) { 62 // TODO: support this feature for non-system LC app. b/23939816 63 return; 64 } 65 if (sFetchTask != null) { 66 sFetchTask.cancel(true); 67 sFetchTask = null; 68 } 69 if (DEBUG) Log.d(TAG, "Request to start fetching logos."); 70 if (channels == null || channels.isEmpty()) { 71 return; 72 } 73 sFetchTask = new FetchLogoTask(context.getApplicationContext(), channels); 74 sFetchTask.execute(); 75 } 76 ChannelLogoFetcher()77 private ChannelLogoFetcher() {} 78 79 private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> { 80 private final Context mContext; 81 private final List<Channel> mChannels; 82 FetchLogoTask(Context context, List<Channel> channels)83 private FetchLogoTask(Context context, List<Channel> channels) { 84 mContext = context; 85 mChannels = channels; 86 } 87 88 @Override doInBackground(Void... arg)89 protected Void doInBackground(Void... arg) { 90 if (isCancelled()) { 91 if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); 92 return null; 93 } 94 List<Channel> channelsToUpdate = new ArrayList<>(); 95 List<Channel> channelsToRemove = new ArrayList<>(); 96 // Updates or removes the logo by comparing the logo uri which is got from the cloud 97 // and the stored one. And we assume that the data got form the cloud is 100% 98 // correct and completed. 99 SharedPreferences sharedPreferences = 100 mContext.getSharedPreferences( 101 SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS, 102 Context.MODE_PRIVATE); 103 SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit(); 104 Map<String, ?> uncheckedChannels = sharedPreferences.getAll(); 105 boolean isFirstTimeFetchChannelLogo = 106 sharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true); 107 // Iterating channels. 108 for (Channel channel : mChannels) { 109 String channelIdString = Long.toString(channel.getId()); 110 String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString); 111 if (!TextUtils.isEmpty(channel.getLogoUri()) 112 && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) { 113 channelsToUpdate.add(channel); 114 sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri()); 115 } else if (TextUtils.isEmpty(channel.getLogoUri()) 116 && (!TextUtils.isEmpty(storedChannelLogoUri) 117 || isFirstTimeFetchChannelLogo)) { 118 channelsToRemove.add(channel); 119 sharedPreferencesEditor.remove(channelIdString); 120 } 121 } 122 123 // Removes non existing channels from SharedPreferences. 124 for (String channelId : uncheckedChannels.keySet()) { 125 sharedPreferencesEditor.remove(channelId); 126 } 127 128 // Updates channel logos. 129 for (Channel channel : channelsToUpdate) { 130 if (isCancelled()) { 131 if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); 132 return null; 133 } 134 // Downloads the channel logo. 135 String logoUri = channel.getLogoUri(); 136 ScaledBitmapInfo bitmapInfo = 137 BitmapUtils.decodeSampledBitmapFromUriString( 138 mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE); 139 if (bitmapInfo == null) { 140 Log.e( 141 TAG, 142 "Failed to load bitmap. {channelName=" 143 + channel.getDisplayName() 144 + ", " 145 + "logoUri=" 146 + logoUri 147 + "}"); 148 continue; 149 } 150 if (isCancelled()) { 151 if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); 152 return null; 153 } 154 155 // Inserts the logo to DB. 156 Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId()); 157 try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) { 158 bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os); 159 } catch (IOException e) { 160 Log.e(TAG, "Failed to write " + logoUri + " to " + dstLogoUri, e); 161 // Removes it from the shared preference for the failed channels to make it 162 // retry next time. 163 sharedPreferencesEditor.remove(Long.toString(channel.getId())); 164 continue; 165 } 166 if (DEBUG) { 167 Log.d( 168 TAG, 169 "Inserting logo file to DB succeeded. {from=" 170 + logoUri 171 + ", to=" 172 + dstLogoUri 173 + "}"); 174 } 175 } 176 177 // Removes the logos for the channels that have logos before but now 178 // their logo uris are null. 179 boolean deleteChannelLogoFailed = false; 180 if (!channelsToRemove.isEmpty()) { 181 ArrayList<ContentProviderOperation> ops = new ArrayList<>(); 182 for (Channel channel : channelsToRemove) { 183 ops.add( 184 ContentProviderOperation.newDelete( 185 TvContract.buildChannelLogoUri(channel.getId())) 186 .build()); 187 } 188 try { 189 mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); 190 } catch (RemoteException | OperationApplicationException e) { 191 deleteChannelLogoFailed = true; 192 Log.e(TAG, "Error deleting obsolete channels", e); 193 } 194 } 195 if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) { 196 sharedPreferencesEditor.putBoolean( 197 PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false); 198 } 199 sharedPreferencesEditor.commit(); 200 if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully."); 201 return null; 202 } 203 204 @Override onPostExecute(Void result)205 protected void onPostExecute(Void result) { 206 sFetchTask = null; 207 } 208 } 209 } 210