@@ -0,0 +1,122 @@
package com.github.zinfidel.sf4dailydigest;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.support.v7.app.ActionBarActivity;
import android.widget.Toast;

import java.util.HashMap;
import java.util.Map;


/** The settings activity, complete with action bar. */
public class SettingsActivity extends ActionBarActivity {

/** See getCharPrefMap(). */
private static Map<String, String> charPrefMap = null;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
getFragmentManager().beginTransaction().replace(android.R.id.content,
new SettingsFragment()).commit();
}

// TODO: DEBUGGING REMOVE ME IMMEDIATELY
@Override
protected void onResume() {
super.onResume();
MainActivity.prefsChanged = true;
}

/** Returns a mapping of character ids to preference keys.*/
public static Map<String, String> getCharPrefMap() {
// Initialize the singleton map if it doesn't exist yet.
if (charPrefMap == null) {
charPrefMap = new HashMap<>();
for (String id : Character.allChars) {
String key = id + "_enabled_key";
charPrefMap.put(id, key);
}
}

return charPrefMap;
}

/** Settings fragment for embedding in the Settings Activity. */
public static class SettingsFragment extends PreferenceFragment {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

addPreferencesFromResource(R.xml.preferences);

// Dynamically add checkboxes with character names and icon.
PreferenceScreen prefChars = (PreferenceScreen) findPreference("button_characters_key");
for (String id : Character.allChars) {
Character c = Character.get(id);
CheckBoxPreference cbp = new CheckBoxPreference(getActivity());
cbp.setKey(getCharPrefMap().get(id));
cbp.setTitle(c.name);
cbp.setIcon(c.icon);
cbp.setDefaultValue(true);
prefChars.addPreference(cbp);
}

// Add click listeners to the reset buttons.
Preference turnOff = (Preference) findPreference("turn_off_all_chars_key");
turnOff.setOnPreferenceClickListener(new PrefCharToggleListener(getActivity(), false));
Preference turnOn = (Preference) findPreference("turn_on_all_chars_key");
turnOn.setOnPreferenceClickListener(new PrefCharToggleListener(getActivity(), true));
}

/** Listener for the special toggle button preferences. */
private class PrefCharToggleListener implements Preference.OnPreferenceClickListener {

private final Context context;
private final boolean enabled;

public PrefCharToggleListener(Context context, boolean enabled) {
this.context = context;
this.enabled = enabled;
}

/** Toggles all character preferences at once. Emits a toast afterwards. */
@Override
public boolean onPreferenceClick(Preference preference) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor ed = sp.edit();

// Update the preference values AND the checkbox visual states.
for (String id : Character.allChars) {
String key = getCharPrefMap().get(id);

ed.putBoolean(key, enabled);

CheckBoxPreference cbp = (CheckBoxPreference) findPreference(key);
cbp.setChecked(enabled);
}
ed.apply();

// Display a toast indicating that the operation executed.
Resources res = context.getResources();
CharSequence toastText = enabled ? res.getText(R.string.pref_all_chars_on_toast)
: res.getText(R.string.pref_all_chars_off_toast);
Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show();
return true;
}
}
}


}
@@ -1,17 +1,21 @@
package com.github.zinfidel.sf4dailydigest;

import android.content.Context;
import android.util.DisplayMetrics;

import com.google.api.client.util.DateTime;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.youtube.YouTube;
import com.google.api.services.youtube.model.SearchListResponse;
import com.google.api.services.youtube.model.SearchResult;
import com.google.api.services.youtube.model.ThumbnailDetails;

import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
@@ -21,20 +25,30 @@
*/
public class YouTubeConnector {

private static YouTube instance = null;
private static String apiKey = null;

public static Calendar lastSearch = null;
private static final long HOUR_IN_MIILIS = 60L * 60L * 1000L;

private static final String KEY_FILE = "youtube_api_key.txt";
private static final String THUMB_RES_DEF = "default";
private static final String THUMB_RES_MED = "medium";

private static final String PARTS = "id, snippet";
private static final String QUERY_TYPE = "video";
private static final long MAX_RESULTS = 5;
private static final String FIELDS = "items(id/videoId,snippet(title,thumbnails(default(url))))";
private static final String PREFIX = "USF4";
private static final String ORDER = "date";
private static final long MAX_RESULTS = 10;
private static final String FIELDS =
"items(id/videoId,snippet(title,publishedAt,channelTitle,thumbnails/%s/url))";
private static final String PREFIX = "USF4 ";

private static String apiKey = null;
private final String thumbRes;
private final YouTube youTube;
private YouTube.Search.List query;

public YouTubeConnector(Context context) {
apiKey = getKey(context);
thumbRes = getThumbRes(context);
youTube = new YouTube.Builder(new NetHttpTransport(), new JacksonFactory(), null)
.setApplicationName(context.getResources().getString(R.string.app_name))
.build();
@@ -43,8 +57,11 @@ public YouTubeConnector(Context context) {
query = youTube.search().list(PARTS);
query.setKey(apiKey);
query.setType(QUERY_TYPE);
query.setOrder(ORDER);
query.setMaxResults(MAX_RESULTS);
query.setFields(FIELDS);
lastSearch = Calendar.getInstance();
query.setPublishedAfter(getYesterday());
query.setFields(String.format(FIELDS, thumbRes));
} catch (IOException ex) {
// TODO: display error message
System.exit(-1);
@@ -79,11 +96,11 @@ public List<VideoItem> search(String keywords) {
* srs/main/assets/youtube_api_key.txt.
* @return YouTube API key.
*/
private static final String getKey(Context context) {
private static String getKey(Context context) {
if (apiKey == null) {
String key = null;
try {
key = IOUtils.toString(context.getAssets().open("youtube_api_key.txt"));
key = IOUtils.toString(context.getAssets().open(KEY_FILE));
} catch (IOException ex) {
// Unrecoverable - either the key was not copied to assets, or something went very wrong
// while accessing the file system. In any case, the application can not continue.
@@ -96,16 +113,57 @@ private static final String getKey(Context context) {
return apiKey;
}

/**
* Determines the size of thumbnail to request from YouTube based on screen pixel density.
* @param context Application context.
* @return The query string for the thumbnail size.
*/
private static String getThumbRes(Context context) {
int density = context.getResources().getDisplayMetrics().densityDpi;
return density <= DisplayMetrics.DENSITY_HIGH ? THUMB_RES_DEF : THUMB_RES_MED;
}

/**
* @return Yesterday's date (24 hours ago) in RFC 3339 format.
*/
private static DateTime getYesterday() {
Calendar c = Calendar.getInstance();
c.roll(Calendar.DAY_OF_YEAR, -1);
return new DateTime(c.getTime());
}

/**
* Returns the difference, in hours, between the supplied publish date and the last search.
* @param publish The publish date of the video.
* @return The delta in ours between the publish date and the last search.
*/
private static int getSearchDelta(long publish) {
long delta = lastSearch.getTimeInMillis() - publish;
return (int) (delta / HOUR_IN_MIILIS);
}


/** Struct-like class that stores search results in an easy-to-use format. */
public class VideoItem {

public final String id;
public final String title;
public final String channel;
public final int published;
public final String thumbUrl;

public VideoItem(SearchResult sr) {
id = sr.getId().getVideoId();
title = sr.getSnippet().getTitle();
thumbUrl = sr.getSnippet().getThumbnails().getDefault().getUrl();
channel = sr.getSnippet().getChannelTitle();
published = getSearchDelta(sr.getSnippet().getPublishedAt().getValue());

ThumbnailDetails thumbs = sr.getSnippet().getThumbnails();
if (thumbs.getDefault() != null) {
thumbUrl = thumbs.getDefault().getUrl();
} else {
thumbUrl = thumbs.getMedium().getUrl();
}
}
}

@@ -1,19 +1,23 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<com.github.zinfidel.sf4dailydigest.ButtonBarView
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_width="wrap_content"
android:layout_height="match_parent"/>

<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent"
android:paddingTop="@dimen/listview_spacing"
android:paddingLeft="@dimen/listview_spacing"
android:dividerHeight="@dimen/listview_spacing"
android:divider="@android:color/transparent"/>

</LinearLayout>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/preference_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SettingsActivity"/>
@@ -6,12 +6,29 @@

<ImageView
android:id="@+id/listitem_thumbnail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
android:layout_width="112dp"
android:layout_height="63dp"
android:scaleType="centerCrop"/>

<TextView
android:id="@+id/listitem_title"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="@dimen/listview_spacing"
android:layout_marginStart="@dimen/listview_spacing">

<TextView
style="@style/VideoTitle"
android:id="@+id/listitem_title"/>

<TextView
style="@style/VideoSubtitle"
android:id="@+id/listitem_channel"/>

<TextView
style="@style/VideoSubtitle"
android:id="@+id/listitem_published"/>

</LinearLayout>

</LinearLayout>
@@ -3,9 +3,10 @@

<LinearLayout
android:id="@+id/buttons_container"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:background="@android:color/darker_gray"/>
android:orientation="vertical"
android:background="#222222"/>

</merge>
@@ -1,6 +1,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
<item android:id="@+id/action_settings" android:title="@string/action_settings"
android:orderInCategory="100" app:showAsAction="never" />
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">

<item
android:id="@+id/action_settings"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>

This file was deleted.

@@ -2,4 +2,6 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>

<dimen name="listview_spacing">8dp</dimen>
</resources>
@@ -2,4 +2,10 @@
<string name="app_name">SF4 Daily Digest</string>
<string name="action_settings">Settings</string>
<string name="youtube_search">http://www.youtube.com/watch?v=</string>

<string name="pref_character_select">Select Characters</string>
<string name="pref_all_chars_off">Turn Off All Characters</string>
<string name="pref_all_chars_on">Turn On All Characters</string>
<string name="pref_all_chars_off_toast">All Characters Turned Off.</string>
<string name="pref_all_chars_on_toast">All Characters Turned On.</string>
</resources>
@@ -2,7 +2,29 @@

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>

<!-- Settings theme. Required to avoid crash! -->
<!--<style name="SettingsTheme" parent="Theme.AppCompat.Light.DarkActionBar">-->
<!--<item name="android:windowNoTitle">false</item>-->
<!--<item name="android:windowActionBar">true</item>-->
<!--<item name="android:actionBarStyle">@style/ActionBarTheme</item>-->
<!--</style>-->

<!-- Style for title text fields on video items. -->
<style name="VideoTitle" parent="@android:style/TextAppearance.Small">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">12sp</item>
<item name="android:textStyle">bold</item>
</style>

<!-- Style for sub text fields on video items. -->
<style name="VideoSubtitle" parent="@android:style/TextAppearance.Small">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">12sp</item>
<item name="android:textColor">?android:textColorSecondary</item>
</style>

</resources>
@@ -86,7 +86,7 @@
</character>

<character id="decapre">
<name>decapre</name>
<name>Decapre</name>
<search>
<string>decapre</string>
<string>DE</string>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:key="button_characters_key"
android:title="@string/pref_character_select"
android:persistent="true">

<!-- Character checkboxes here - generated dynamically in SettingsActivity.java. -->

</PreferenceScreen>

<Preference
android:key="turn_off_all_chars_key"
android:title="@string/pref_all_chars_off"/>

<Preference
android:key="turn_on_all_chars_key"
android:title="@string/pref_all_chars_on"/>

</PreferenceScreen>