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
17package com.android.tv.data;
18
19import android.content.ContentProviderOperation;
20import android.content.Context;
21import android.content.OperationApplicationException;
22import android.content.SharedPreferences;
23import android.graphics.Bitmap.CompressFormat;
24import android.media.tv.TvContract;
25import android.net.Uri;
26import android.os.AsyncTask;
27import android.os.RemoteException;
28import android.support.annotation.MainThread;
29import android.text.TextUtils;
30import android.util.Log;
31
32import com.android.tv.common.SharedPreferencesUtils;
33import com.android.tv.util.BitmapUtils;
34import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
35import com.android.tv.util.PermissionUtils;
36
37import java.io.IOException;
38import java.io.OutputStream;
39import java.util.ArrayList;
40import java.util.Map;
41import 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 */
47public 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
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
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
86        private FetchLogoTask(Context context, List<Channel> channels) {
87            mContext = context;
88            mChannels = channels;
89        }
90
91        @Override
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
194        protected void onPostExecute(Void result) {
195            sFetchTask = null;
196        }
197    }
198}
199