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