• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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