• 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 
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