1 /*
2  * Copyright (C) 2017 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.example.partnersupportsampletvinput;
18 
19 import android.app.Activity;
20 import android.app.FragmentManager;
21 import android.content.ContentResolver;
22 import android.content.ContentValues;
23 import android.os.AsyncTask;
24 import android.os.Bundle;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.Nullable;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.util.Pair;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 
34 import androidx.leanback.app.GuidedStepFragment;
35 import androidx.leanback.widget.GuidanceStylist;
36 import androidx.leanback.widget.GuidanceStylist.Guidance;
37 import androidx.leanback.widget.GuidedAction;
38 
39 import com.google.android.tv.partner.support.EpgContract;
40 import com.google.android.tv.partner.support.EpgInput;
41 import com.google.android.tv.partner.support.EpgInputs;
42 import com.google.android.tv.partner.support.Lineup;
43 import com.google.android.tv.partner.support.Lineups;
44 
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.Collections;
48 import java.util.Comparator;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.regex.Pattern;
53 
54 /** Lineup Selection Fragment for users to select a lineup */
55 public class LineupSelectionFragment extends GuidedStepFragment {
56     public static final boolean DEBUG = false;
57     public static final String TAG = "LineupSelectionFragment";
58 
59     public static final String KEY_TITLE = "title";
60     public static final String KEY_DESCRIPTION = "description";
61     private static final long ACTION_ID_STATUS = 1;
62     private static final long ACTION_ID_LINEUPS = 2;
63     private static final Pattern CHANNEL_NUMBER_DELIMITER = Pattern.compile("([ .-])");
64     private Activity mActivity;
65     private GuidedAction mStatusAction;
66     private String mPostcode;
67     private List<String> mChannelNumbers;
68     private Map<GuidedAction, Lineup> mActionLineupMap = new HashMap<>();
69 
70     private AsyncTask<Void, Void, List<Pair<Lineup, Integer>>> mFetchLineupTask;
71 
72     @Override
onCreate(Bundle savedInstanceState)73     public void onCreate(Bundle savedInstanceState) {
74         mActivity = getActivity();
75         mStatusAction =
76                 new GuidedAction.Builder(mActivity)
77                         .id(ACTION_ID_STATUS)
78                         .title("Loading lineups")
79                         .description("please wait")
80                         .focusable(false)
81                         .build();
82 
83         mFetchLineupTask =
84                 new AsyncTask<Void, Void, List<Pair<Lineup, Integer>>>() {
85                     @Override
86                     protected List<Pair<Lineup, Integer>> doInBackground(Void... voids) {
87                         return lineupChannelMatchCount(getLineups(), getChannelNumbers());
88                     }
89 
90                     @Override
91                     protected void onPostExecute(List<Pair<Lineup, Integer>> result) {
92                         List<GuidedAction> actions = new ArrayList<>();
93                         if (result.isEmpty()) {
94                             mStatusAction.setTitle("No lineup found for postcode: " + mPostcode);
95                             mStatusAction.setDescription("");
96                             mStatusAction.setFocusable(true);
97                             notifyActionChanged(findActionPositionById(ACTION_ID_STATUS));
98                             return;
99                         }
100                         mActionLineupMap = new HashMap<>();
101                         for (Pair<Lineup, Integer> pair : result) {
102                             Lineup lineup = pair.first;
103                             String title =
104                                     TextUtils.isEmpty(lineup.getName())
105                                             ? lineup.getId()
106                                             : lineup.getName();
107                             GuidedAction action =
108                                     new GuidedAction.Builder(getActivity())
109                                             .id(ACTION_ID_LINEUPS)
110                                             .title(title)
111                                             .description(pair.second + " channels match")
112                                             .build();
113                             mActionLineupMap.put(action, lineup);
114                             actions.add(action);
115                         }
116                         setActions(actions);
117                     }
118                 };
119 
120         super.onCreate(savedInstanceState);
121     }
122 
123     @NonNull
124     @Override
onCreateGuidance(Bundle savedInstanceState)125     public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
126         return new Guidance(
127                 "LineupSelectionFragment", "LineupSelectionFragment Description", null, null);
128     }
129 
130     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)131     public View onCreateView(
132             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
133         View v = super.onCreateView(inflater, container, savedInstanceState);
134         if (!mFetchLineupTask.isCancelled()) {
135             mPostcode = getArguments().getString(ChannelScanFragment.KEY_POSTCODE);
136             mChannelNumbers =
137                     getArguments().getStringArrayList(ChannelScanFragment.KEY_CHANNEL_NUMBERS);
138             mFetchLineupTask.execute();
139         }
140         return v;
141     }
142 
143     @Override
onDestroyView()144     public void onDestroyView() {
145         mFetchLineupTask.cancel(true);
146         super.onDestroyView();
147     }
148 
149     @Override
onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)150     public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
151         actions.add(mStatusAction);
152     }
153 
154     @Override
onGuidedActionClicked(GuidedAction action)155     public void onGuidedActionClicked(GuidedAction action) {
156         if (action == null) {
157             return;
158         }
159         switch ((int) action.getId()) {
160             case (int) ACTION_ID_STATUS:
161                 finishGuidedStepFragments();
162                 break;
163             case (int) ACTION_ID_LINEUPS:
164                 FragmentManager fm = getFragmentManager();
165                 super.onGuidedActionClicked(action);
166                 onLineupSelected(mActionLineupMap.get(action));
167                 ResultFragment fragment = new ResultFragment();
168                 Bundle args = new Bundle();
169                 args.putString(KEY_TITLE, action.getTitle().toString());
170                 args.putString(KEY_DESCRIPTION, action.getDescription().toString());
171                 fragment.setArguments(args);
172                 GuidedStepFragment.add(fm, fragment);
173                 break;
174             default:
175         }
176     }
177 
lineupChannelMatchCount( List<Lineup> lineups, List<String> localChannels)178     private List<Pair<Lineup, Integer>> lineupChannelMatchCount(
179             List<Lineup> lineups, List<String> localChannels) {
180         List<Pair<Lineup, Integer>> result = new ArrayList<>();
181         for (Lineup lineup : lineups) {
182             result.add(Pair.create(lineup, getMatchCount(lineup.getChannels(), localChannels)));
183         }
184         // sort in decreasing order
185         Collections.sort(
186                 result,
187                 new Comparator<Pair<Lineup, Integer>>() {
188                     @Override
189                     public int compare(Pair<Lineup, Integer> pair, Pair<Lineup, Integer> other) {
190                         return Integer.compare(other.second, pair.second);
191                     }
192                 });
193         return result;
194     }
195 
getMatchCount(List<String> lineupChannels, List<String> localChannels)196     private static int getMatchCount(List<String> lineupChannels, List<String> localChannels) {
197         int count = 0;
198         for (String lineupChannel : lineupChannels) {
199             if (TextUtils.isEmpty(lineupChannel)) {
200                 continue;
201             }
202             List<String> parsedNumbers = parseChannelNumber(lineupChannel);
203             for (String channel : localChannels) {
204                 if (TextUtils.isEmpty(channel)) {
205                     continue;
206                 }
207                 if (matchChannelNumber(parsedNumbers, parseChannelNumber(channel))) {
208                     if (DEBUG) {
209                         Log.d(TAG, lineupChannel + " matches " + channel);
210                     }
211                     count++;
212                     break;
213                 }
214             }
215         }
216         return count;
217     }
218 
getLineups()219     private List<Lineup> getLineups() {
220         return new ArrayList<>(Lineups.query(mActivity.getContentResolver(), mPostcode));
221     }
222 
getChannelNumbers()223     private List<String> getChannelNumbers() {
224         return mChannelNumbers;
225     }
226 
onLineupSelected(@ullable Lineup lineup)227     private void onLineupSelected(@Nullable Lineup lineup) {
228         if (lineup == null) {
229             return;
230         }
231         ContentValues values = new ContentValues();
232         values.put(EpgContract.EpgInputs.COLUMN_INPUT_ID, SampleTvInputService.INPUT_ID);
233         values.put(EpgContract.EpgInputs.COLUMN_LINEUP_ID, lineup.getId());
234 
235         ContentResolver contentResolver = getActivity().getContentResolver();
236         EpgInput epgInput = EpgInputs.queryEpgInput(contentResolver, SampleTvInputService.INPUT_ID);
237         if (epgInput == null) {
238             contentResolver.insert(EpgContract.EpgInputs.CONTENT_URI, values);
239         } else {
240             values.put(EpgContract.EpgInputs.COLUMN_ID, epgInput.getId());
241             EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values));
242         }
243     }
244 
245     /**
246      * Parses the channel number string to a list of numbers (major number, minor number, etc.).
247      *
248      * @param channelNumber the display number of the channel
249      * @return a list of numbers
250      */
parseChannelNumber(String channelNumber)251     private static List<String> parseChannelNumber(String channelNumber) {
252         // TODO: add tests for this method
253         List<String> numbers =
254                 new ArrayList<>(
255                         Arrays.asList(TextUtils.split(channelNumber, CHANNEL_NUMBER_DELIMITER)));
256         numbers.removeAll(Collections.singleton(""));
257         if (numbers.size() < 1 || numbers.size() > 2) {
258             Log.w(TAG, "unsupported channel number format: " + channelNumber);
259             return new ArrayList<>();
260         }
261         return numbers;
262     }
263 
264     /**
265      * Checks whether two lists of channel numbers match or not. If the sizes are different,
266      * additional elements are ignore.
267      */
matchChannelNumber(List<String> numbers, List<String> other)268     private static boolean matchChannelNumber(List<String> numbers, List<String> other) {
269         if (numbers.isEmpty() || other.isEmpty()) {
270             return false;
271         }
272         int i = 0;
273         int j = 0;
274         while (i < numbers.size() && j < other.size()) {
275             if (!numbers.get(i++).equals(other.get(j++))) {
276                 return false;
277             }
278         }
279         return true;
280     }
281 }
282