diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index b57f791769d3..e23ede6219ef 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -101,6 +101,7 @@ loadCategories(int id) { Cursor c = db.query(CATEGORIES_TABLE, new String[] { "id", "wp_id", - "category_name" }, "blog_id=" + id, null, null, null, null); + "category_name" }, "blog_id=" + Integer.toString(id), null, null, null, null); int numRows = c.getCount(); c.moveToFirst(); List returnVector = new Vector(); diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java new file mode 100644 index 000000000000..e872d4274843 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/datasets/SiteSettingsTable.java @@ -0,0 +1,104 @@ +package org.wordpress.android.datasets; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import org.wordpress.android.WordPress; +import org.wordpress.android.models.CategoryModel; +import org.wordpress.android.models.SiteSettingsModel; + +import java.util.HashMap; +import java.util.Map; + +public final class SiteSettingsTable { + public static final String CATEGORIES_TABLE_NAME = "site_categories"; + + private static final String CREATE_CATEGORIES_TABLE_SQL = + "CREATE TABLE IF NOT EXISTS " + + CATEGORIES_TABLE_NAME + + " (" + + CategoryModel.ID_COLUMN_NAME + " INTEGER PRIMARY KEY, " + + CategoryModel.NAME_COLUMN_NAME + " TEXT, " + + CategoryModel.SLUG_COLUMN_NAME + " TEXT, " + + CategoryModel.DESC_COLUMN_NAME + " TEXT, " + + CategoryModel.PARENT_ID_COLUMN_NAME + " INTEGER, " + + CategoryModel.POST_COUNT_COLUMN_NAME + " INTEGER" + + ");"; + + public static void createTable(SQLiteDatabase db) { + if (db != null) { + db.execSQL(SiteSettingsModel.CREATE_SETTINGS_TABLE_SQL); + db.execSQL(CREATE_CATEGORIES_TABLE_SQL); + } + } + + public static Map getAllCategories() { + String sqlCommand = sqlSelectAllCategories() + ";"; + Cursor cursor = WordPress.wpDB.getDatabase().rawQuery(sqlCommand, null); + + if (cursor == null || !cursor.moveToFirst() || cursor.getCount() == 0) return null; + + Map models = new HashMap<>(); + for (int i = 0; i < cursor.getCount(); ++i) { + CategoryModel model = new CategoryModel(); + model.deserializeFromDatabase(cursor); + models.put(model.id, model); + cursor.moveToNext(); + } + + return models; + } + + public static Cursor getCategory(long id) { + if (id < 0) return null; + + String sqlCommand = sqlSelectAllCategories() + sqlWhere(CategoryModel.ID_COLUMN_NAME, Long.toString(id)) + ";"; + return WordPress.wpDB.getDatabase().rawQuery(sqlCommand, null); + } + + public static Cursor getSettings(long id) { + if (id < 0) return null; + + String sqlCommand = sqlSelectAllSettings() + sqlWhere(SiteSettingsModel.ID_COLUMN_NAME, Long.toString(id)) + ";"; + return WordPress.wpDB.getDatabase().rawQuery(sqlCommand, null); + } + + public static void saveCategory(CategoryModel category) { + if (category == null) return; + + ContentValues values = category.serializeToDatabase(); + category.isInLocalTable = WordPress.wpDB.getDatabase().insertWithOnConflict( + CATEGORIES_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1; + } + + public static void saveCategories(CategoryModel[] categories) { + if (categories == null) return; + + for (CategoryModel category : categories) { + saveCategory(category); + } + } + + public static void saveSettings(SiteSettingsModel settings) { + if (settings == null) return; + + ContentValues values = settings.serializeToDatabase(); + settings.isInLocalTable = WordPress.wpDB.getDatabase().insertWithOnConflict( + SiteSettingsModel.SETTINGS_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1; + + saveCategories(settings.categories); + } + + private static String sqlSelectAllCategories() { + return "SELECT * FROM " + CATEGORIES_TABLE_NAME + " "; + } + + private static String sqlSelectAllSettings() { + return "SELECT * FROM " + SiteSettingsModel.SETTINGS_TABLE_NAME + " "; + } + + private static String sqlWhere(String variable, String value) { + return "WHERE " + variable + "=\"" + value + "\" "; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java b/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java new file mode 100644 index 000000000000..77b08bfae2ab --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java @@ -0,0 +1,65 @@ +package org.wordpress.android.models; + +import android.content.ContentValues; +import android.database.Cursor; + +/** + * Represents WordPress post Category data and handles local database (de)serialization. + */ +public class CategoryModel { + // Categories table column names + public static final String ID_COLUMN_NAME = "ID"; + public static final String NAME_COLUMN_NAME = "name"; + public static final String SLUG_COLUMN_NAME = "slug"; + public static final String DESC_COLUMN_NAME = "description"; + public static final String PARENT_ID_COLUMN_NAME = "parent"; + public static final String POST_COUNT_COLUMN_NAME = "post_count"; + + public int id; + public String name; + public String slug; + public String description; + public int parentId; + public int postCount; + public boolean isInLocalTable; + + public CategoryModel() { + id = -1; + name = ""; + slug = ""; + description = ""; + parentId = -1; + postCount = 0; + isInLocalTable = false; + } + + /** + * Sets data from a local database {@link Cursor}. + */ + public void deserializeFromDatabase(Cursor cursor) { + if (cursor == null) return; + + id = cursor.getInt(cursor.getColumnIndex(ID_COLUMN_NAME)); + name = cursor.getString(cursor.getColumnIndex(NAME_COLUMN_NAME)); + slug = cursor.getString(cursor.getColumnIndex(SLUG_COLUMN_NAME)); + description = cursor.getString(cursor.getColumnIndex(DESC_COLUMN_NAME)); + parentId = cursor.getInt(cursor.getColumnIndex(PARENT_ID_COLUMN_NAME)); + postCount = cursor.getInt(cursor.getColumnIndex(POST_COUNT_COLUMN_NAME)); + isInLocalTable = true; + } + + /** + * Creates the {@link ContentValues} object to store this category data in a local database. + */ + public ContentValues serializeToDatabase() { + ContentValues values = new ContentValues(); + values.put(ID_COLUMN_NAME, id); + values.put(NAME_COLUMN_NAME, name); + values.put(SLUG_COLUMN_NAME, slug); + values.put(DESC_COLUMN_NAME, description); + values.put(PARENT_ID_COLUMN_NAME, parentId); + values.put(POST_COUNT_COLUMN_NAME, postCount); + + return values; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java b/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java new file mode 100644 index 000000000000..e37db5973dd7 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java @@ -0,0 +1,411 @@ +package org.wordpress.android.models; + +import android.content.ContentValues; +import android.database.Cursor; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Holds blog settings and provides methods to (de)serialize .com and self-hosted network calls. + */ + +public class SiteSettingsModel { + public static final int RELATED_POSTS_ENABLED_FLAG = 0x1; + public static final int RELATED_POST_HEADER_FLAG = 0x2; + public static final int RELATED_POST_IMAGE_FLAG = 0x4; + + // Settings table column names + public static final String ID_COLUMN_NAME = "id"; + public static final String ADDRESS_COLUMN_NAME = "address"; + public static final String USERNAME_COLUMN_NAME = "username"; + public static final String PASSWORD_COLUMN_NAME = "password"; + public static final String TITLE_COLUMN_NAME = "title"; + public static final String TAGLINE_COLUMN_NAME = "tagline"; + public static final String LANGUAGE_COLUMN_NAME = "language"; + public static final String PRIVACY_COLUMN_NAME = "privacy"; + public static final String LOCATION_COLUMN_NAME = "location"; + public static final String DEF_CATEGORY_COLUMN_NAME = "defaultCategory"; + public static final String DEF_POST_FORMAT_COLUMN_NAME = "defaultPostFormat"; + public static final String CATEGORIES_COLUMN_NAME = "categories"; + public static final String POST_FORMATS_COLUMN_NAME = "postFormats"; + public static final String CREDS_VERIFIED_COLUMN_NAME = "credsVerified"; + public static final String RELATED_POSTS_COLUMN_NAME = "relatedPosts"; + public static final String ALLOW_COMMENTS_COLUMN_NAME = "allowComments"; + public static final String SEND_PINGBACKS_COLUMN_NAME = "sendPingbacks"; + public static final String RECEIVE_PINGBACKS_COLUMN_NAME = "receivePingbacks"; + public static final String SHOULD_CLOSE_AFTER_COLUMN_NAME = "shouldCloseAfter"; + public static final String CLOSE_AFTER_COLUMN_NAME = "closeAfter"; + public static final String SORT_BY_COLUMN_NAME = "sortBy"; + public static final String SHOULD_THREAD_COLUMN_NAME = "shouldThread"; + public static final String THREADING_COLUMN_NAME = "threading"; + public static final String SHOULD_PAGE_COLUMN_NAME = "shouldPage"; + public static final String PAGING_COLUMN_NAME = "paging"; + public static final String MANUAL_APPROVAL_COLUMN_NAME = "manualApproval"; + public static final String IDENTITY_REQUIRED_COLUMN_NAME = "identityRequired"; + public static final String USER_ACCOUNT_REQUIRED_COLUMN_NAME = "userAccountRequired"; + public static final String WHITELIST_COLUMN_NAME = "whitelist"; + public static final String MODERATION_KEYS_COLUMN_NAME = "moderationKeys"; + public static final String BLACKLIST_KEYS_COLUMN_NAME = "blacklistKeys"; + + public static final String SETTINGS_TABLE_NAME = "site_settings"; + public static final String CREATE_SETTINGS_TABLE_SQL = + "CREATE TABLE IF NOT EXISTS " + + SETTINGS_TABLE_NAME + + " (" + + ID_COLUMN_NAME + " INTEGER PRIMARY KEY, " + + ADDRESS_COLUMN_NAME + " TEXT, " + + USERNAME_COLUMN_NAME + " TEXT, " + + PASSWORD_COLUMN_NAME + " TEXT, " + + TITLE_COLUMN_NAME + " TEXT, " + + TAGLINE_COLUMN_NAME + " TEXT, " + + LANGUAGE_COLUMN_NAME + " INTEGER, " + + PRIVACY_COLUMN_NAME + " INTEGER, " + + LOCATION_COLUMN_NAME + " BOOLEAN, " + + DEF_CATEGORY_COLUMN_NAME + " TEXT, " + + DEF_POST_FORMAT_COLUMN_NAME + " TEXT, " + + CATEGORIES_COLUMN_NAME + " TEXT, " + + POST_FORMATS_COLUMN_NAME + " TEXT, " + + CREDS_VERIFIED_COLUMN_NAME + " BOOLEAN, " + + RELATED_POSTS_COLUMN_NAME + " INTEGER, " + + ALLOW_COMMENTS_COLUMN_NAME + " BOOLEAN, " + + SEND_PINGBACKS_COLUMN_NAME + " BOOLEAN, " + + RECEIVE_PINGBACKS_COLUMN_NAME + " BOOLEAN, " + + SHOULD_CLOSE_AFTER_COLUMN_NAME + " BOOLEAN, " + + CLOSE_AFTER_COLUMN_NAME + " INTEGER, " + + SORT_BY_COLUMN_NAME + " INTEGER, " + + SHOULD_THREAD_COLUMN_NAME + " BOOLEAN, " + + THREADING_COLUMN_NAME + " INTEGER, " + + SHOULD_PAGE_COLUMN_NAME + " BOOLEAN, " + + PAGING_COLUMN_NAME + " INTEGER, " + + MANUAL_APPROVAL_COLUMN_NAME + " BOOLEAN, " + + IDENTITY_REQUIRED_COLUMN_NAME + " BOOLEAN, " + + USER_ACCOUNT_REQUIRED_COLUMN_NAME + " BOOLEAN, " + + WHITELIST_COLUMN_NAME + " BOOLEAN, " + + MODERATION_KEYS_COLUMN_NAME + " TEXT, " + + BLACKLIST_KEYS_COLUMN_NAME + " TEXT" + + ");"; + + public boolean isInLocalTable; + public boolean hasVerifiedCredentials; + public long localTableId; + public String address; + public String username; + public String password; + public String title; + public String tagline; + public String language; + public int languageId; + public int privacy; + public boolean location; + public int defaultCategory; + public CategoryModel[] categories; + public String defaultPostFormat; + public Map postFormats; + public String[] postFormatKeys; + public boolean showRelatedPosts; + public boolean showRelatedPostHeader; + public boolean showRelatedPostImages; + public boolean allowComments; + public boolean sendPingbacks; + public boolean receivePingbacks; + public boolean shouldCloseAfter; + public int closeCommentAfter; + public int sortCommentsBy; + public boolean shouldThreadComments; + public int threadingLevels; + public boolean shouldPageComments; + public int commentsPerPage; + public boolean commentApprovalRequired; + public boolean commentsRequireIdentity; + public boolean commentsRequireUserAccount; + public boolean commentAutoApprovalKnownUsers; + public int maxLinks; + public List holdForModeration; + public List blacklist; + + @Override + public boolean equals(Object other) { + if (!(other instanceof SiteSettingsModel)) return false; + SiteSettingsModel otherModel = (SiteSettingsModel) other; + + return localTableId == otherModel.localTableId && + address.equals(otherModel.address) && + username.equals(otherModel.username) && + password.equals(otherModel.password) && + title.equals(otherModel.title) && + tagline.equals(otherModel.tagline) && + languageId == otherModel.languageId && + privacy == otherModel.privacy && + location == otherModel.location && + defaultPostFormat.equals(otherModel.defaultPostFormat) && + defaultCategory == otherModel.defaultCategory && + showRelatedPosts == otherModel.showRelatedPosts && + showRelatedPostHeader == otherModel.showRelatedPostHeader && + showRelatedPostImages == otherModel.showRelatedPostImages && + allowComments == otherModel.allowComments && + sendPingbacks == otherModel.sendPingbacks && + receivePingbacks == otherModel.receivePingbacks && + closeCommentAfter == otherModel.closeCommentAfter && + sortCommentsBy == otherModel.sortCommentsBy && + threadingLevels == otherModel.threadingLevels && + commentsPerPage == otherModel.commentsPerPage && + commentApprovalRequired == otherModel.commentApprovalRequired && + commentsRequireIdentity == otherModel.commentsRequireIdentity && + commentsRequireUserAccount == otherModel.commentsRequireUserAccount && + commentAutoApprovalKnownUsers == otherModel.commentAutoApprovalKnownUsers && + maxLinks == otherModel.maxLinks && + holdForModeration != null && holdForModeration.equals(otherModel.holdForModeration) && + blacklist != null && blacklist.equals(otherModel.blacklist); + } + + /** + * Copies data from another {@link SiteSettingsModel}. + */ + public void copyFrom(SiteSettingsModel other) { + if (other == null) return; + + isInLocalTable = other.isInLocalTable; + hasVerifiedCredentials = other.hasVerifiedCredentials; + localTableId = other.localTableId; + address = other.address; + username = other.username; + password = other.password; + title = other.title; + tagline = other.tagline; + language = other.language; + languageId = other.languageId; + privacy = other.privacy; + location = other.location; + defaultCategory = other.defaultCategory; + categories = other.categories; + defaultPostFormat = other.defaultPostFormat; + postFormats = other.postFormats; + showRelatedPosts = other.showRelatedPosts; + showRelatedPostHeader = other.showRelatedPostHeader; + showRelatedPostImages = other.showRelatedPostImages; + allowComments = other.allowComments; + sendPingbacks = other.sendPingbacks; + receivePingbacks = other.receivePingbacks; + shouldCloseAfter = other.shouldCloseAfter; + closeCommentAfter = other.closeCommentAfter; + sortCommentsBy = other.sortCommentsBy; + shouldThreadComments = other.shouldThreadComments; + threadingLevels = other.threadingLevels; + shouldPageComments = other.shouldPageComments; + commentsPerPage = other.commentsPerPage; + commentApprovalRequired = other.commentApprovalRequired; + commentsRequireIdentity = other.commentsRequireIdentity; + commentsRequireUserAccount = other.commentsRequireUserAccount; + commentAutoApprovalKnownUsers = other.commentAutoApprovalKnownUsers; + maxLinks = other.maxLinks; + holdForModeration = new ArrayList<>(other.holdForModeration); + blacklist = new ArrayList<>(other.blacklist); + } + + /** + * Sets values from a local database {@link Cursor}. + */ + public void deserializeOptionsDatabaseCursor(Cursor cursor, Map models) { + if (cursor == null || !cursor.moveToFirst() || cursor.getCount() == 0) return; + + localTableId = getIntFromCursor(cursor, ID_COLUMN_NAME); + address = getStringFromCursor(cursor, ADDRESS_COLUMN_NAME); + username = getStringFromCursor(cursor, USERNAME_COLUMN_NAME); + password = getStringFromCursor(cursor, PASSWORD_COLUMN_NAME); + title = getStringFromCursor(cursor, TITLE_COLUMN_NAME); + tagline = getStringFromCursor(cursor, TAGLINE_COLUMN_NAME); + languageId = getIntFromCursor(cursor, LANGUAGE_COLUMN_NAME); + privacy = getIntFromCursor(cursor, PRIVACY_COLUMN_NAME); + defaultCategory = getIntFromCursor(cursor, DEF_CATEGORY_COLUMN_NAME); + defaultPostFormat = getStringFromCursor(cursor, DEF_POST_FORMAT_COLUMN_NAME); + location = getBooleanFromCursor(cursor, LOCATION_COLUMN_NAME); + hasVerifiedCredentials = getBooleanFromCursor(cursor, CREDS_VERIFIED_COLUMN_NAME); + allowComments = getBooleanFromCursor(cursor, ALLOW_COMMENTS_COLUMN_NAME); + sendPingbacks = getBooleanFromCursor(cursor, SEND_PINGBACKS_COLUMN_NAME); + receivePingbacks = getBooleanFromCursor(cursor, RECEIVE_PINGBACKS_COLUMN_NAME); + shouldCloseAfter = getBooleanFromCursor(cursor, SHOULD_CLOSE_AFTER_COLUMN_NAME); + closeCommentAfter = getIntFromCursor(cursor, CLOSE_AFTER_COLUMN_NAME); + sortCommentsBy = getIntFromCursor(cursor, SORT_BY_COLUMN_NAME); + shouldThreadComments = getBooleanFromCursor(cursor, SHOULD_THREAD_COLUMN_NAME); + threadingLevels = getIntFromCursor(cursor, THREADING_COLUMN_NAME); + shouldPageComments = getBooleanFromCursor(cursor, SHOULD_PAGE_COLUMN_NAME); + commentsPerPage = getIntFromCursor(cursor, PAGING_COLUMN_NAME); + commentApprovalRequired = getBooleanFromCursor(cursor, MANUAL_APPROVAL_COLUMN_NAME); + commentsRequireIdentity = getBooleanFromCursor(cursor, IDENTITY_REQUIRED_COLUMN_NAME); + commentsRequireUserAccount = getBooleanFromCursor(cursor, USER_ACCOUNT_REQUIRED_COLUMN_NAME); + commentAutoApprovalKnownUsers = getBooleanFromCursor(cursor, WHITELIST_COLUMN_NAME); + + String moderationKeys = getStringFromCursor(cursor, MODERATION_KEYS_COLUMN_NAME); + String blacklistKeys = getStringFromCursor(cursor, BLACKLIST_KEYS_COLUMN_NAME); + holdForModeration = new ArrayList<>(); + blacklist = new ArrayList<>(); + Collections.addAll(holdForModeration, moderationKeys.split("\n")); + Collections.addAll(blacklist, blacklistKeys.split("\n")); + + setRelatedPostsFlags(Math.max(0, getIntFromCursor(cursor, RELATED_POSTS_COLUMN_NAME))); + + String cachedCategories = getStringFromCursor(cursor, CATEGORIES_COLUMN_NAME); + String cachedFormats = getStringFromCursor(cursor, POST_FORMATS_COLUMN_NAME); + if (models != null && !TextUtils.isEmpty(cachedCategories)) { + String[] split = cachedCategories.split(","); + categories = new CategoryModel[split.length]; + for (int i = 0; i < split.length; ++i) { + int catId = Integer.parseInt(split[i]); + categories[i] = models.get(catId); + } + } + if (!TextUtils.isEmpty(cachedFormats)) { + String[] split = cachedFormats.split(";"); + postFormats = new HashMap<>(); + for (String format : split) { + String[] kvp = format.split(","); + postFormats.put(kvp[0], kvp[1]); + } + } + + int cachedRelatedPosts = getIntFromCursor(cursor, RELATED_POSTS_COLUMN_NAME); + if (cachedRelatedPosts != -1) { + setRelatedPostsFlags(cachedRelatedPosts); + } + + isInLocalTable = true; + } + + /** + * Creates the {@link ContentValues} object to store this category data in a local database. + */ + public ContentValues serializeToDatabase() { + ContentValues values = new ContentValues(); + values.put(ID_COLUMN_NAME, localTableId); + values.put(ADDRESS_COLUMN_NAME, address); + values.put(USERNAME_COLUMN_NAME, username); + values.put(PASSWORD_COLUMN_NAME, password); + values.put(TITLE_COLUMN_NAME, title); + values.put(TAGLINE_COLUMN_NAME, tagline); + values.put(PRIVACY_COLUMN_NAME, privacy); + values.put(LANGUAGE_COLUMN_NAME, languageId); + values.put(LOCATION_COLUMN_NAME, location); + values.put(DEF_CATEGORY_COLUMN_NAME, defaultCategory); + values.put(CATEGORIES_COLUMN_NAME, categoryIdList(categories)); + values.put(DEF_POST_FORMAT_COLUMN_NAME, defaultPostFormat); + values.put(POST_FORMATS_COLUMN_NAME, postFormatList(postFormats)); + values.put(CREDS_VERIFIED_COLUMN_NAME, hasVerifiedCredentials); + values.put(RELATED_POSTS_COLUMN_NAME, getRelatedPostsFlags()); + values.put(ALLOW_COMMENTS_COLUMN_NAME, allowComments); + values.put(SEND_PINGBACKS_COLUMN_NAME, sendPingbacks); + values.put(RECEIVE_PINGBACKS_COLUMN_NAME, receivePingbacks); + values.put(SHOULD_CLOSE_AFTER_COLUMN_NAME, shouldCloseAfter); + values.put(CLOSE_AFTER_COLUMN_NAME, closeCommentAfter); + values.put(SORT_BY_COLUMN_NAME, sortCommentsBy); + values.put(SHOULD_THREAD_COLUMN_NAME, shouldThreadComments); + values.put(THREADING_COLUMN_NAME, threadingLevels); + values.put(SHOULD_PAGE_COLUMN_NAME, shouldPageComments); + values.put(PAGING_COLUMN_NAME, commentsPerPage); + values.put(MANUAL_APPROVAL_COLUMN_NAME, commentApprovalRequired); + values.put(IDENTITY_REQUIRED_COLUMN_NAME, commentsRequireIdentity); + values.put(USER_ACCOUNT_REQUIRED_COLUMN_NAME, commentsRequireUserAccount); + values.put(WHITELIST_COLUMN_NAME, commentAutoApprovalKnownUsers); + + String moderationKeys = ""; + if (holdForModeration != null) { + for (String key : holdForModeration) { + moderationKeys += key + "\n"; + } + } + String blacklistKeys = ""; + if (blacklist != null) { + for (String key : blacklist) { + blacklistKeys += key + "\n"; + } + } + values.put(MODERATION_KEYS_COLUMN_NAME, moderationKeys); + values.put(BLACKLIST_KEYS_COLUMN_NAME, blacklistKeys); + + return values; + } + + public int getRelatedPostsFlags() { + int flags = 0; + + if (showRelatedPosts) flags |= RELATED_POSTS_ENABLED_FLAG; + if (showRelatedPostHeader) flags |= RELATED_POST_HEADER_FLAG; + if (showRelatedPostImages) flags |= RELATED_POST_IMAGE_FLAG; + + return flags; + } + + public void setRelatedPostsFlags(int flags) { + showRelatedPosts = (flags & RELATED_POSTS_ENABLED_FLAG) > 0; + showRelatedPostHeader = (flags & RELATED_POST_HEADER_FLAG) > 0; + showRelatedPostImages = (flags & RELATED_POST_IMAGE_FLAG) > 0; + } + + /** + * Used to serialize post formats to store in a local database. + * + * @param formats + * map of post formats where the key is the format ID and the value is the format name + * @return + * a String of semi-colon separated KVP's of Post Formats; Post Format ID -> Post Format Name + */ + private static String postFormatList(Map formats) { + if (formats == null || formats.size() == 0) return ""; + + StringBuilder builder = new StringBuilder(); + for (String key : formats.keySet()) { + builder.append(key).append(",").append(formats.get(key)).append(";"); + } + builder.setLength(builder.length() - 1); + + return builder.toString(); + } + + /** + * Used to serialize categories to store in a local database. + * + * @param elements + * {@link CategoryModel} array to create String ID list from + * @return + * a String of comma-separated integer Category ID's + */ + private static String categoryIdList(CategoryModel[] elements) { + if (elements == null || elements.length == 0) return ""; + + StringBuilder builder = new StringBuilder(); + for (CategoryModel element : elements) { + builder.append(String.valueOf(element.id)).append(","); + } + builder.setLength(builder.length() - 1); + + return builder.toString(); + } + + /** + * Helper method to get an integer value from a given column in a Cursor. + */ + private int getIntFromCursor(Cursor cursor, String columnName) { + int columnIndex = cursor.getColumnIndex(columnName); + return columnIndex != -1 ? cursor.getInt(columnIndex) : -1; + } + + /** + * Helper method to get a String value from a given column in a Cursor. + */ + private String getStringFromCursor(Cursor cursor, String columnName) { + int columnIndex = cursor.getColumnIndex(columnName); + return columnIndex != -1 ? cursor.getString(columnIndex) : ""; + } + + /** + * Helper method to get a boolean value (stored as an int) from a given column in a Cursor. + */ + private boolean getBooleanFromCursor(Cursor cursor, String columnName) { + int columnIndex = cursor.getColumnIndex(columnName); + return columnIndex != -1 && cursor.getInt(columnIndex) != 0; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index aa59a625250c..09a0cf0c3b94 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -28,6 +28,7 @@ import org.wordpress.android.ui.posts.PostsListActivity; import org.wordpress.android.ui.prefs.BlogPreferencesActivity; import org.wordpress.android.ui.prefs.SettingsActivity; +import org.wordpress.android.ui.prefs.SiteSettingsInterface; import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsActivity; import org.wordpress.android.ui.stats.StatsActivity; import org.wordpress.android.ui.stats.StatsConstants; @@ -139,8 +140,10 @@ public static void viewPostPreviewForResult(Activity activity, Post post, boolea public static void addNewBlogPostOrPageForResult(Activity context, Blog blog, boolean isPage) { if (blog == null) return; - // Create a new post object + // Create a new post object and assign default settings Post newPost = new Post(blog.getLocalTableBlogId(), isPage); + newPost.setCategories("[" + SiteSettingsInterface.getDefaultCategory(context) +"]"); + newPost.setPostFormat(SiteSettingsInterface.getDefaultFormat(context)); WordPress.wpDB.savePost(newPost); Intent intent = new Intent(context, EditPostActivity.class); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WPNumberPicker.java b/WordPress/src/main/java/org/wordpress/android/ui/WPNumberPicker.java new file mode 100644 index 000000000000..404c4aa67255 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/WPNumberPicker.java @@ -0,0 +1,262 @@ +package org.wordpress.android.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import android.widget.EditText; +import android.widget.NumberPicker; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.util.WPPrefUtils; + +import java.lang.reflect.Field; + +public class WPNumberPicker extends NumberPicker { + private static final String DIVIDER_FIELD = "mSelectionDivider"; + private static final String INPUT_FIELD = "mInputText"; + private static final String INDICES_FIELD = "mSelectorIndices"; + private static final String CUR_OFFSET_FIELD = "mCurrentScrollOffset"; + private static final String SELECTOR_HEIGHT_FIELD = "mSelectorElementHeight"; + private static final String INITIAL_OFFSET_FIELD = "mInitialScrollOffset"; + private static final String CURRENT_OFFSET_FIELD = "mCurrentScrollOffset"; + private static final String PAINT_FIELD = "mSelectorWheelPaint"; + + private static final int DISPLAY_COUNT = 5; + private static final int MIDDLE_INDEX = 2; + + private Field mOffsetField; + private Field mSelectorHeight; + private Field mSelectorIndices; + private Field mInitialOffset; + private Field mCurrentOffset; + + private EditText mInputView; + private Formatter mFormatter; + private Paint mPaint; + private int[] mDisplayValues; + + public WPNumberPicker(Context context, AttributeSet attrs) { + super(context, attrs); + mDisplayValues = new int[DISPLAY_COUNT]; + getFieldsViaReflection(); + } + + @Override + public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) { + super.addView(child, index, params); + if (child instanceof TextView) { + WPPrefUtils.layoutAsNumberPickerPeek((TextView) child); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateIntitialOffset(); + setVerticalFadingEdgeEnabled(false); + setHorizontalFadingEdgeEnabled(false); + mInputView.setVisibility(View.INVISIBLE); + } + + @Override + public void setValue(int value) { + if (value < getMinValue()) value = getMinValue(); + if (value > getMaxValue()) value = getMaxValue(); + super.setValue(value); + EditText view = (EditText) getChildAt(0); + WPPrefUtils.layoutAsNumberPickerSelected(view); + } + + @Override + protected void onDraw(Canvas canvas) { + int[] selectorIndices = getIndices(); + setIndices(new int[0]); + setIndices(selectorIndices); + + // Draw the middle number with a different font + setDisplayValues(); + float elementHeight = getSelectorElementHeight(); + float x = ((getRight() - getLeft()) / 2.0f); + float y = getScrollOffset(); + Paint paint = mInputView.getPaint(); + paint.setTextAlign(Paint.Align.CENTER); + //noinspection deprecation + paint.setColor(getResources().getColor(R.color.blue_medium)); + int alpha = isEnabled() ? 255 : 96; + paint.setAlpha(alpha); + mPaint.setAlpha(alpha); + + int offset = getResources().getDimensionPixelSize(R.dimen.margin_medium); + // Draw the visible values + for (int i = 0; i < DISPLAY_COUNT; ++i) { + String scrollSelectorValue; + if (mFormatter != null) { + scrollSelectorValue = mFormatter.format(mDisplayValues[i]); + } else { + scrollSelectorValue = String.valueOf(mDisplayValues[i]); + } + if (i == MIDDLE_INDEX) { + canvas.drawText(scrollSelectorValue, x, y - ((paint.descent() + paint.ascent()) / 2) - offset, paint); + } else { + canvas.drawText(scrollSelectorValue, x, y - ((mPaint.descent() + mPaint.ascent()) / 2) - offset, mPaint); + } + y += elementHeight; + } + } + + @Override + public void setFormatter(Formatter formatter) { + super.setFormatter(formatter); + mFormatter = formatter; + } + + private void setDisplayValues() { + int value = getValue(); + for (int i = 0; i < DISPLAY_COUNT; ++i) { + mDisplayValues[i] = value - MIDDLE_INDEX + i; + if (mDisplayValues[i] < getMinValue()) { + mDisplayValues[i] = getMaxValue() + (mDisplayValues[i] + 1 - getMinValue()); + } else if (mDisplayValues[i] > getMaxValue()) { + mDisplayValues[i] = getMinValue() + (mDisplayValues[i] - getMaxValue() - 1); + } + } + } + + private void setIndices(int[] indices) { + if (mSelectorIndices != null) { + try { + mSelectorIndices.set(this, indices); + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + private int[] getIndices() { + if (mSelectorIndices != null) { + try { + return (int[]) mSelectorIndices.get(this); + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + return null; + } + + private int getScrollOffset() { + if (mOffsetField != null) { + try { + return (Integer) mOffsetField.get(this); + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + return 0; + } + + private int getSelectorElementHeight() { + if (mSelectorHeight != null) { + try { + return (Integer) mSelectorHeight.get(this); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + return 0; + } + + private void updateIntitialOffset() { + if (mInitialOffset != null) { + try { + int offset = (Integer) mInitialOffset.get(this) - getSelectorElementHeight(); + mInitialOffset.set(this, offset); + // Only do this once + mInitialOffset = null; + + if (mCurrentOffset != null) { + mCurrentOffset.set(this, offset); + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + /** + * From https://www.snip2code.com/Snippet/67740/NumberPicker-with-transparent-selection- + */ + private void removeDividers(Class clazz) { + Field selectionDivider = getFieldAndSetAccessible(clazz, DIVIDER_FIELD); + if (selectionDivider != null) { + try { + selectionDivider.set(this, null); + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + private void getTextPaint(Class clazz) { + Field paint = getFieldAndSetAccessible(clazz, PAINT_FIELD); + if (paint != null) { + try { + mPaint = (Paint) paint.get(this); + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + private void getInputField(Class clazz) { + Field inputField = getFieldAndSetAccessible(clazz, INPUT_FIELD); + if (inputField != null) { + try { + mInputView = ((EditText) inputField.get(this)); + } catch (IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + /** + * Gets a class field using reflection and makes it accessible. + */ + private Field getFieldAndSetAccessible(Class clazz, String fieldName) { + Field field = null; + try { + field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + + return field; + } + + private void getFieldsViaReflection() { + Class numberPickerClass = null; + try { + numberPickerClass = Class.forName(NumberPicker.class.getName()); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + if (numberPickerClass == null) return; + + mSelectorHeight = getFieldAndSetAccessible(numberPickerClass, SELECTOR_HEIGHT_FIELD); + mOffsetField = getFieldAndSetAccessible(numberPickerClass, CUR_OFFSET_FIELD); + mSelectorIndices = getFieldAndSetAccessible(numberPickerClass, INDICES_FIELD); + mInitialOffset = getFieldAndSetAccessible(numberPickerClass, INITIAL_OFFSET_FIELD); + mCurrentOffset = getFieldAndSetAccessible(numberPickerClass, CURRENT_OFFSET_FIELD); + + getTextPaint(numberPickerClass); + getInputField(numberPickerClass); + removeDividers(numberPickerClass); + setIndices(new int[DISPLAY_COUNT]); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/AbstractFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/AbstractFragment.java index 2fe69bb94c59..a1ffdca60af8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/AbstractFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/AbstractFragment.java @@ -10,7 +10,6 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ImageView; @@ -25,6 +24,7 @@ import org.wordpress.android.networking.RestClientUtils; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.WPActivityUtils; /** * A fragment representing a single step in a wizard. The fragment shows a dummy title indicating @@ -75,12 +75,8 @@ protected boolean onDoneEvent(int actionId, KeyEvent event) { } // hide keyboard before calling the done action - InputMethodManager inputManager = (InputMethodManager) getActivity().getSystemService( - Context.INPUT_METHOD_SERVICE); View view = getActivity().getCurrentFocus(); - if (view != null) { - inputManager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); - } + if (view != null) WPActivityUtils.hideKeyboard(view); // call child action onDoneAction(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java index 03fc2ffd80d6..a8d65a84d0d3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java @@ -48,6 +48,8 @@ public class MySiteFragment extends Fragment private WPTextView mBlogSubtitleTextView; private LinearLayout mLookAndFeelHeader; private RelativeLayout mThemesContainer; + private View mConfigurationHeader; + private View mSettingsView; private View mFabView; private LinearLayout mNoSiteView; private ScrollView mScrollView; @@ -116,6 +118,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, mBlogSubtitleTextView = (WPTextView) rootView.findViewById(R.id.my_site_subtitle_label); mLookAndFeelHeader = (LinearLayout) rootView.findViewById(R.id.my_site_look_and_feel_header); mThemesContainer = (RelativeLayout) rootView.findViewById(R.id.row_themes); + mConfigurationHeader = rootView.findViewById(R.id.row_configuration); + mSettingsView = rootView.findViewById(R.id.row_settings); mScrollView = (ScrollView) rootView.findViewById(R.id.scroll_view); mNoSiteView = (LinearLayout) rootView.findViewById(R.id.no_site_view); mNoSiteDrakeImageView = (ImageView) rootView.findViewById(R.id.my_site_no_site_view_drake); @@ -186,7 +190,7 @@ public void onClick(View v) { } }); - rootView.findViewById(R.id.row_settings).setOnClickListener(new View.OnClickListener() { + mSettingsView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ActivityLauncher.viewBlogSettingsForResult(getActivity(), mBlog); @@ -304,6 +308,11 @@ private void refreshBlogDetails() { mLookAndFeelHeader.setVisibility(themesVisibility); mThemesContainer.setVisibility(themesVisibility); + // show settings for all self-hosted to expose Delete Site + int settingsVisibility = mBlog.isAdmin() || !mBlog.isDotcomFlag() ? View.VISIBLE : View.GONE; + mConfigurationHeader.setVisibility(settingsVisibility); + mSettingsView.setVisibility(settingsVisibility); + mBlavatarImageView.setImageUrl(GravatarUtils.blavatarFromUrl(mBlog.getUrl(), mBlavatarSz), WPNetworkImageView.ImageType.BLAVATAR); String blogName = StringUtils.unescapeHTML(mBlog.getBlogName()); @@ -346,4 +355,14 @@ public void onStart() { public void onEventMainThread(CoreEvents.MainViewPagerScrolled event) { mFabView.setTranslationY(mFabTargetYTranslation * event.mXOffset); } + + @SuppressWarnings("unused") + public void onEventMainThread(CoreEvents.BlogListChanged event) { + if (!isAdded() || (mBlog = WordPress.getBlog(mBlog.getLocalTableBlogId())) == null) return; + + // Update view if blog has a new name + if (!mBlogTitleTextView.getText().equals(mBlog.getBlogName())) { + mBlogTitleTextView.setText(mBlog.getBlogName()); + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java index b7f76ae5416e..c40ab4356230 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java @@ -35,6 +35,7 @@ import org.wordpress.android.ui.stats.datasets.StatsTable; import org.wordpress.android.util.CoreEvents; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.WPActivityUtils; import org.xmlrpc.android.ApiHelper; import de.greenrobot.event.EventBus; @@ -318,8 +319,7 @@ private void disableSearchMode() { private void hideSoftKeyboard() { if (!hasHardwareKeyboard()) { - InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(mSearchView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + WPActivityUtils.hideKeyboard(mSearchView); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index e79628baa9cb..9fd6f4c3274c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -33,8 +33,8 @@ import org.wordpress.android.ui.notifications.utils.NotificationsUtils; import org.wordpress.android.ui.notifications.utils.SimperiumUtils; import org.wordpress.android.ui.prefs.AppPrefs; -import org.wordpress.android.ui.prefs.BlogPreferencesActivity; import org.wordpress.android.ui.prefs.SettingsFragment; +import org.wordpress.android.ui.prefs.SiteSettingsFragment; import org.wordpress.android.ui.reader.ReaderPostListFragment; import org.wordpress.android.util.AnalyticsUtils; import org.wordpress.android.util.AniUtils; @@ -385,7 +385,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } break; case RequestCodes.BLOG_SETTINGS: - if (resultCode == BlogPreferencesActivity.RESULT_BLOG_REMOVED) { + if (resultCode == SiteSettingsFragment.RESULT_BLOG_REMOVED) { // user removed the current (self-hosted) blog from blog settings if (!AccountHelper.isSignedIn()) { ActivityLauncher.showSignInForResult(this); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java index 06e456ab6828..d3a3487f80dc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java @@ -60,6 +60,7 @@ import org.wordpress.android.ui.media.services.MediaEvents; import org.wordpress.android.ui.media.services.MediaUploadService; import org.wordpress.android.ui.posts.services.PostUploadService; +import org.wordpress.android.ui.prefs.SiteSettingsInterface; import org.wordpress.android.ui.suggestion.adapters.TagSuggestionAdapter; import org.wordpress.android.ui.suggestion.util.SuggestionServiceConnectionManager; import org.wordpress.android.ui.suggestion.util.SuggestionUtils; @@ -210,6 +211,8 @@ protected void onCreate(Bundle savedInstanceState) { // Create a new post for share intents and QuickPress mPost = new Post(WordPress.getCurrentLocalTableBlogId(), false); + mPost.setCategories("[" + SiteSettingsInterface.getDefaultCategory(this) +"]"); + mPost.setPostFormat(SiteSettingsInterface.getDefaultFormat(this)); WordPress.wpDB.savePost(mPost); mIsNewPost = true; } else if (extras != null) { @@ -691,6 +694,10 @@ public void onBackPressed() { } } + public boolean isNewPost() { + return mIsNewPost; + } + private boolean hasEmptyContentFields() { return TextUtils.isEmpty(mEditorFragment.getTitle()) && TextUtils.isEmpty(mEditorFragment.getContent()); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java index e2c0908a5b86..463e4dd16a29 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java @@ -2,6 +2,7 @@ import android.app.AlertDialog; import android.app.Fragment; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.location.Address; @@ -45,6 +46,7 @@ import org.wordpress.android.models.PostLocation; import org.wordpress.android.models.PostStatus; import org.wordpress.android.ui.RequestCodes; +import org.wordpress.android.ui.prefs.SiteSettingsInterface; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.EditTextUtils; @@ -188,7 +190,7 @@ public boolean onTouch(View view, MotionEvent motionEvent) { initSettingsFields(); populateSelectedCategories(); - initLocation(); + initLocation(mRootView); return mRootView; } @@ -612,12 +614,11 @@ public void afterTextChanged(Editable s) { * called when activity is created to initialize the location provider, show views related * to location if enabled for this blog, and retrieve the current location if necessary */ - public void initLocation() { - if (!mPost.supportsLocation()) { - return; - } + private void initLocation(ViewGroup rootView) { + if (!mPost.supportsLocation()) return; + // show the location views if a provider was found and this is a post on a blog that has location enabled - View locationRootView = ((ViewStub) mRootView.findViewById(R.id.stub_post_location_settings)).inflate(); + View locationRootView = ((ViewStub) rootView.findViewById(R.id.stub_post_location_settings)).inflate(); TextView locationLabel = ((TextView) locationRootView.findViewById(R.id.locationLabel)); locationLabel.setText(getResources().getString(R.string.location).toUpperCase()); @@ -644,13 +645,21 @@ public void initLocation() { updateLocation.setOnClickListener(this); removeLocation.setOnClickListener(this); + if (!checkForLocationPermission()) return; + // if this post has location attached to it, look up the location address if (mPost.hasLocation()) { showLocationView(); PostLocation location = mPost.getLocation(); setLocation(location.getLatitude(), location.getLongitude()); } else { - showLocationAdd(); + // Search for current location to geotag post if preferences allow + EditPostActivity activity = (EditPostActivity) getActivity(); + if (SiteSettingsInterface.getGeotagging(activity) && activity.isNewPost()) { + searchLocation(); + } else { + showLocationAdd(); + } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java index 9726f5a00cc5..46a57ba697a5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java @@ -1,5 +1,7 @@ package org.wordpress.android.ui.prefs; +import android.app.Fragment; +import android.app.FragmentManager; import android.app.AlertDialog; import android.content.DialogInterface; import android.os.Bundle; @@ -28,6 +30,7 @@ import org.wordpress.android.util.CoreEvents.UserSignedOutCompletely; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.networking.ConnectionChangeReceiver; import de.greenrobot.event.EventBus; @@ -35,9 +38,11 @@ * Activity for configuring blog specific settings. */ public class BlogPreferencesActivity extends AppCompatActivity { - public static final String ARG_LOCAL_BLOG_ID = "local_blog_id"; + public static final String ARG_LOCAL_BLOG_ID = SiteSettingsFragment.ARG_LOCAL_BLOG_ID; public static final int RESULT_BLOG_REMOVED = RESULT_FIRST_USER; + private static final String KEY_SETTINGS_FRAGMENT = "settings-fragment"; + // The blog this activity is managing settings for. private Blog blog; private boolean mBlogDeleted; @@ -53,49 +58,69 @@ public class BlogPreferencesActivity extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.blog_preferences); Integer id = getIntent().getIntExtra(ARG_LOCAL_BLOG_ID, -1); blog = WordPress.getBlog(id); - - if (blog == null) { + if (WordPress.getBlog(id) == null) { Toast.makeText(this, getString(R.string.blog_not_found), Toast.LENGTH_SHORT).show(); finish(); return; } - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(StringUtils.unescapeHTML(blog.getNameOrHostUrl())); - actionBar.setDisplayHomeAsUpEnabled(true); - } - - mUsernameET = (EditText) findViewById(R.id.username); - mPasswordET = (EditText) findViewById(R.id.password); - mHttpUsernameET = (EditText) findViewById(R.id.httpuser); - mHttpPasswordET = (EditText) findViewById(R.id.httppassword); - mScaledImageWidthET = (EditText) findViewById(R.id.scaledImageWidth); - mFullSizeCB = (CheckBox) findViewById(R.id.fullSizeImage); - mScaledCB = (CheckBox) findViewById(R.id.scaledImage); - mImageWidthSpinner = (Spinner) findViewById(R.id.maxImageWidth); - Button removeBlogButton = (Button) findViewById(R.id.remove_account); - - // remove blog & credentials apply only to dot org if (blog.isDotcomFlag()) { - View credentialsRL = findViewById(R.id.sectionContent); - credentialsRL.setVisibility(View.GONE); - removeBlogButton.setVisibility(View.GONE); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setCustomView(R.layout.site_settings_actionbar); + } + + FragmentManager fragmentManager = getFragmentManager(); + Fragment siteSettingsFragment = fragmentManager.findFragmentByTag(KEY_SETTINGS_FRAGMENT); + + if (siteSettingsFragment == null) { + siteSettingsFragment = new SiteSettingsFragment(); + siteSettingsFragment.setArguments(getIntent().getExtras()); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, siteSettingsFragment, KEY_SETTINGS_FRAGMENT) + .commit(); + } } else { - removeBlogButton.setVisibility(View.VISIBLE); - removeBlogButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - removeBlogWithConfirmation(); - } - }); - } + setContentView(R.layout.blog_preferences); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(StringUtils.unescapeHTML(blog.getNameOrHostUrl())); + actionBar.setDisplayHomeAsUpEnabled(true); + } - loadSettingsForBlog(); + mUsernameET = (EditText) findViewById(R.id.username); + mPasswordET = (EditText) findViewById(R.id.password); + mHttpUsernameET = (EditText) findViewById(R.id.httpuser); + mHttpPasswordET = (EditText) findViewById(R.id.httppassword); + mScaledImageWidthET = (EditText) findViewById(R.id.scaledImageWidth); + mFullSizeCB = (CheckBox) findViewById(R.id.fullSizeImage); + mScaledCB = (CheckBox) findViewById(R.id.scaledImage); + mImageWidthSpinner = (Spinner) findViewById(R.id.maxImageWidth); + Button removeBlogButton = (Button) findViewById(R.id.remove_account); + + // remove blog & credentials apply only to dot org + if (blog.isDotcomFlag()) { + View credentialsRL = findViewById(R.id.sectionContent); + credentialsRL.setVisibility(View.GONE); + removeBlogButton.setVisibility(View.GONE); + } else { + removeBlogButton.setVisibility(View.VISIBLE); + removeBlogButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeBlogWithConfirmation(); + } + }); + } + + loadSettingsForBlog(); + } } @Override @@ -108,7 +133,7 @@ public void finish() { protected void onPause() { super.onPause(); - if (mBlogDeleted) { + if (blog.isDotcomFlag() || mBlogDeleted) { return; } @@ -159,6 +184,18 @@ public void onClick(DialogInterface dialog, int whichButton) { } } + @Override + protected void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + } + + @Override + protected void onStop() { + EventBus.getDefault().unregister(this); + super.onStop(); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { int itemID = item.getItemId(); @@ -170,6 +207,24 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } + @SuppressWarnings("unused") + public void onEventMainThread(ConnectionChangeReceiver.ConnectionChangeEvent event) { + FragmentManager fragmentManager = getFragmentManager(); + SiteSettingsFragment siteSettingsFragment = + (SiteSettingsFragment) fragmentManager.findFragmentByTag(KEY_SETTINGS_FRAGMENT); + + if (siteSettingsFragment != null) { + if (!event.isConnected()) { + ToastUtils.showToast(this, getString(R.string.site_settings_disconnected_toast)); + } + + // TODO: add this back when delete blog is back + //https://github.com/wordpress-mobile/WordPress-Android/commit/6a90e3fe46e24ee40abdc4a7f8f0db06f157900c + // Checks for stats widgets that were synched with a blog that could be gone now. +// StatsWidgetProvider.updateWidgetsOnLogout(this); + } + } + private void loadSettingsForBlog() { ArrayAdapter spinnerArrayAdapter = new ArrayAdapter(this, R.layout.simple_spinner_item, new String[]{ diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java new file mode 100644 index 000000000000..68a976fb6332 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java @@ -0,0 +1,283 @@ +package org.wordpress.android.ui.prefs; + +import android.content.res.Resources; +import android.support.annotation.NonNull; +import android.support.v7.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.os.Bundle; +import android.preference.ListPreference; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.RadioButton; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.widgets.TypefaceCache; + +/** + * Custom {@link ListPreference} used to display detail text per item. + */ + +public class DetailListPreference extends ListPreference + implements PreferenceHint { + private DetailListAdapter mListAdapter; + private String[] mDetails; + private String mStartingValue; + private int mSelectedIndex; + private String mHint; + private AlertDialog mDialog; + private int mWhichButtonClicked; + + public DetailListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DetailListPreference); + + for (int i = 0; i < array.getIndexCount(); ++i) { + int index = array.getIndex(i); + if (index == R.styleable.DetailListPreference_entryDetails) { + int id = array.getResourceId(index, -1); + if (id != -1) { + mDetails = array.getResources().getStringArray(id); + } + } else if (index == R.styleable.DetailListPreference_longClickHint) { + mHint = array.getString(index); + } + } + + array.recycle(); + + mSelectedIndex = -1; + } + + @Override + protected void onBindView(@NonNull View view) { + super.onBindView(view); + + setupView((TextView) view.findViewById(android.R.id.title), + R.dimen.text_sz_large, R.color.grey_dark, R.color.grey_lighten_10); + setupView((TextView) view.findViewById(android.R.id.summary), + R.dimen.text_sz_medium, R.color.grey_darken_10, R.color.grey_lighten_10); + } + + @Override + protected void showDialog(Bundle state) { + Context context = getContext(); + Resources res = context.getResources(); + AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Calypso_AlertDialog); + + mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE; + builder.setPositiveButton(R.string.ok, this); + builder.setNegativeButton(res.getString(R.string.cancel).toUpperCase(), this); + + if (mDetails == null) { + mDetails = new String[getEntries() == null ? 1 : getEntries().length]; + } + + mListAdapter = new DetailListAdapter(getContext(), R.layout.detail_list_preference, mDetails); + mStartingValue = getValue(); + mSelectedIndex = findIndexOfValue(mStartingValue); + + builder.setSingleChoiceItems(mListAdapter, mSelectedIndex, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (mSelectedIndex != which) { + mSelectedIndex = which; + mListAdapter.notifyDataSetChanged(); + setValue(getEntryValues()[mSelectedIndex].toString()); + notifyChanged(); + } + } + }); + + View titleView = View.inflate(getContext(), R.layout.detail_list_preference_title, null); + + if (titleView != null) { + TextView titleText = (TextView) titleView.findViewById(R.id.title); + if (titleText != null) { + titleText.setText(getTitle()); + } + + builder.setCustomTitle(titleView); + } else { + builder.setTitle(getTitle()); + } + + if ((mDialog = builder.create()) == null) return; + + if (state != null) { + mDialog.onRestoreInstanceState(state); + } + mDialog.setOnDismissListener(this); + mDialog.show(); + + ListView listView = mDialog.getListView(); + Button positive = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); + Button negative = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE); + Typeface typeface = TypefaceCache.getTypeface(getContext(), + TypefaceCache.FAMILY_OPEN_SANS, + Typeface.BOLD, + TypefaceCache.VARIATION_LIGHT); + + if (listView != null) { + listView.setDividerHeight(0); + listView.setClipToPadding(true); + listView.setPadding(0, 0, 0, res.getDimensionPixelSize(R.dimen.site_settings_divider_height)); + } + + if (positive != null) { + //noinspection deprecation + positive.setTextColor(res.getColor(R.color.blue_medium)); + positive.setTypeface(typeface); + } + + if (negative != null) { + //noinspection deprecation + negative.setTextColor(res.getColor(R.color.blue_medium)); + negative.setTypeface(typeface); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + mWhichButtonClicked = which; + } + + @Override + public void onDismiss(DialogInterface dialog) { + mDialog = null; + onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + int index = positiveResult ? mSelectedIndex : findIndexOfValue(mStartingValue); + CharSequence[] values = getEntryValues(); + if (values != null && index >= 0 && index < values.length) { + String value = String.valueOf(values[index]); + callChangeListener(value); + } else { + callChangeListener(mStartingValue); + } + } + + @Override + public boolean hasHint() { + return !TextUtils.isEmpty(mHint); + } + + @Override + public String getHint() { + return mHint; + } + + @Override + public void setHint(String hint) { + mHint = hint; + } + + public void refreshAdapter() { + if (mListAdapter != null) { + mListAdapter.notifyDataSetChanged(); + } + } + + public void setDetails(String[] details) { + mDetails = details; + refreshAdapter(); + } + + /** + * Helper method to style the Preference screen view + */ + private void setupView(TextView view, int sizeRes, int enabledColorRes, int disabledColorRes) { + if (view != null) { + Resources res = getContext().getResources(); + Typeface typeface = TypefaceCache.getTypeface(getContext(), + TypefaceCache.FAMILY_OPEN_SANS, + Typeface.NORMAL, + TypefaceCache.VARIATION_NORMAL); + + view.setTypeface(typeface); + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(sizeRes)); + //noinspection deprecation + view.setTextColor(res.getColor(isEnabled() ? enabledColorRes : disabledColorRes)); + } + } + + private class DetailListAdapter extends ArrayAdapter { + public DetailListAdapter(Context context, int resource, String[] objects) { + super(context, resource, objects); + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = View.inflate(getContext(), R.layout.detail_list_preference, null); + } + + final RadioButton radioButton = (RadioButton) convertView.findViewById(R.id.radio); + TextView mainText = (TextView) convertView.findViewById(R.id.main_text); + TextView detailText = (TextView) convertView.findViewById(R.id.detail_text); + + if (mainText != null && getEntries() != null && position < getEntries().length) { + mainText.setText(getEntries()[position]); + mainText.setTypeface(TypefaceCache.getTypeface(getContext(), + TypefaceCache.FAMILY_OPEN_SANS, + Typeface.NORMAL, + TypefaceCache.VARIATION_NORMAL)); + } + + if (detailText != null) { + if (mDetails != null && position < mDetails.length && !TextUtils.isEmpty(mDetails[position])) { + detailText.setVisibility(View.VISIBLE); + detailText.setText(mDetails[position]); + detailText.setTypeface(TypefaceCache.getTypeface(getContext(), + TypefaceCache.FAMILY_OPEN_SANS, + Typeface.NORMAL, + TypefaceCache.VARIATION_NORMAL)); + } else { + detailText.setVisibility(View.GONE); + } + } + + if (radioButton != null) { + radioButton.setChecked(mSelectedIndex == position); + radioButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + changeSelection(radioButton, position); + } + }); + } + + convertView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + changeSelection(radioButton, position); + } + }); + + return convertView; + } + + private void changeSelection(RadioButton radioButton, int position) { + CharSequence[] values = getEntryValues(); + + if (radioButton != null && values != null && position < values.length) { + mSelectedIndex = position; + radioButton.setChecked(true); + callChangeListener(values[position]); + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java new file mode 100644 index 000000000000..ecf13fb5c339 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java @@ -0,0 +1,377 @@ +package org.wordpress.android.ui.prefs; + +import android.app.Activity; + +import com.android.volley.VolleyError; +import com.wordpress.rest.RestRequest; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.WordPress; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.datasets.SiteSettingsTable; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.CategoryModel; +import org.wordpress.android.util.AnalyticsUtils; +import org.wordpress.android.util.AppLog; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +class DotComSiteSettings extends SiteSettingsInterface { + // WP.com REST keys used in response to a settings GET and POST request + public static final String LANGUAGE_ID_KEY = "lang_id"; + public static final String PRIVACY_KEY = "blog_public"; + public static final String URL_KEY = "URL"; + public static final String DEF_CATEGORY_KEY = "default_category"; + public static final String DEF_POST_FORMAT_KEY = "default_post_format"; + public static final String RELATED_POSTS_ALLOWED_KEY = "jetpack_relatedposts_allowed"; + public static final String RELATED_POSTS_ENABLED_KEY = "jetpack_relatedposts_enabled"; + public static final String RELATED_POSTS_HEADER_KEY = "jetpack_relatedposts_show_headline"; + public static final String RELATED_POSTS_IMAGES_KEY = "jetpack_relatedposts_show_thumbnails"; + public static final String ALLOW_COMMENTS_KEY = "default_comment_status"; + public static final String SEND_PINGBACKS_KEY = "default_pingback_flag"; + public static final String RECEIVE_PINGBACKS_KEY = "default_ping_status"; + public static final String CLOSE_OLD_COMMENTS_KEY = "close_comments_for_old_posts"; + public static final String CLOSE_OLD_COMMENTS_DAYS_KEY = "close_comments_days_old"; + public static final String THREAD_COMMENTS_KEY = "thread_comments"; + public static final String THREAD_COMMENTS_DEPTH_KEY = "thread_comments_depth"; + public static final String PAGE_COMMENTS_KEY = "page_comments"; + public static final String PAGE_COMMENT_COUNT_KEY = "comments_per_page"; + public static final String COMMENT_SORT_ORDER_KEY = "comment_order"; + public static final String COMMENT_MODERATION_KEY = "comment_moderation"; + public static final String REQUIRE_IDENTITY_KEY = "require_name_email"; + public static final String REQUIRE_USER_ACCOUNT_KEY = "comment_registration"; + public static final String WHITELIST_KNOWN_USERS_KEY = "comment_whitelist"; + public static final String MAX_LINKS_KEY = "comment_max_links"; + public static final String MODERATION_KEYS_KEY = "moderation_keys"; + public static final String BLACKLIST_KEYS_KEY = "blacklist_keys"; + + // WP.com REST keys used to GET certain site settings + public static final String GET_TITLE_KEY = "name"; + public static final String GET_DESC_KEY = "description"; + + // WP.com REST keys used to POST updates to site settings + private static final String SET_TITLE_KEY = "blogname"; + private static final String SET_DESC_KEY = "blogdescription"; + + // JSON response keys + private static final String SETTINGS_KEY = "settings"; + private static final String UPDATED_KEY = "updated"; + + // WP.com REST keys used in response to a categories GET request + private static final String CAT_ID_KEY = "ID"; + private static final String CAT_NAME_KEY = "name"; + private static final String CAT_SLUG_KEY = "slug"; + private static final String CAT_DESC_KEY = "description"; + private static final String CAT_PARENT_ID_KEY = "parent"; + private static final String CAT_POST_COUNT_KEY = "post_count"; + private static final String CAT_NUM_POSTS_KEY = "found"; + private static final String CATEGORIES_KEY = "categories"; + + /** + * Only instantiated by {@link SiteSettingsInterface}. + */ + DotComSiteSettings(Activity host, Blog blog, SiteSettingsListener listener) { + super(host, blog, listener); + } + + @Override + public void saveSettings() { + super.saveSettings(); + + final Map params = serializeDotComParams(); + if (params == null || params.isEmpty()) return; + + WordPress.getRestClientUtils().setGeneralSiteSettings( + String.valueOf(mBlog.getRemoteBlogId()), new RestRequest.Listener() { + @Override + public void onResponse(JSONObject response) { + AppLog.d(AppLog.T.API, "Site Settings saved remotely"); + notifySavedOnUiThread(null); + mRemoteSettings.copyFrom(mSettings); + + if (response != null) { + JSONObject updated = response.optJSONObject(UPDATED_KEY); + if (updated == null) return; + HashMap properties = new HashMap<>(); + Iterator keys = updated.keys(); + while (keys.hasNext()) { + String currentKey = keys.next(); + Object currentValue = updated.opt(currentKey); + if (currentValue != null) { + properties.put(SAVED_ITEM_PREFIX + currentKey, currentValue); + } + } + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_SAVED_REMOTELY, properties); + } + } + }, new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + AppLog.w(AppLog.T.API, "Error POSTing site settings changes: " + error); + notifySavedOnUiThread(error); + } + }, params); + } + + /** + * Request remote site data via the WordPress REST API. + */ + @Override + protected void fetchRemoteData() { + fetchCategories(); + WordPress.getRestClientUtils().getGeneralSettings( + String.valueOf(mBlog.getRemoteBlogId()), new RestRequest.Listener() { + @Override + public void onResponse(JSONObject response) { + AppLog.d(AppLog.T.API, "Received response to Settings REST request."); + credentialsVerified(true); + + mRemoteSettings.localTableId = mBlog.getRemoteBlogId(); + deserializeDotComRestResponse(mBlog, response); + if (!mRemoteSettings.equals(mSettings)) { + mSettings.copyFrom(mRemoteSettings); + SiteSettingsTable.saveSettings(mSettings); + notifyUpdatedOnUiThread(null); + } + } + }, new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + AppLog.w(AppLog.T.API, "Error response to Settings REST request: " + error); + notifyUpdatedOnUiThread(error); + } + }); + } + + /** + * Sets values from a .com REST response object. + */ + public void deserializeDotComRestResponse(Blog blog, JSONObject response) { + if (blog == null || response == null) return; + JSONObject settingsObject = response.optJSONObject(SETTINGS_KEY); + + mRemoteSettings.username = blog.getUsername(); + mRemoteSettings.password = blog.getPassword(); + mRemoteSettings.address = response.optString(URL_KEY, ""); + mRemoteSettings.title = response.optString(GET_TITLE_KEY, ""); + mRemoteSettings.tagline = response.optString(GET_DESC_KEY, ""); + mRemoteSettings.languageId = settingsObject.optInt(LANGUAGE_ID_KEY, -1); + mRemoteSettings.privacy = settingsObject.optInt(PRIVACY_KEY, -2); + mRemoteSettings.defaultCategory = settingsObject.optInt(DEF_CATEGORY_KEY, 0); + mRemoteSettings.defaultPostFormat = settingsObject.optString(DEF_POST_FORMAT_KEY, "0"); + mRemoteSettings.language = languageIdToLanguageCode(Integer.toString(mRemoteSettings.languageId)); + mRemoteSettings.allowComments = settingsObject.optBoolean(ALLOW_COMMENTS_KEY, true); + mRemoteSettings.sendPingbacks = settingsObject.optBoolean(SEND_PINGBACKS_KEY, false); + mRemoteSettings.receivePingbacks = settingsObject.optBoolean(RECEIVE_PINGBACKS_KEY, true); + mRemoteSettings.shouldCloseAfter = settingsObject.optBoolean(CLOSE_OLD_COMMENTS_KEY, false); + mRemoteSettings.closeCommentAfter = settingsObject.optInt(CLOSE_OLD_COMMENTS_DAYS_KEY, 0); + mRemoteSettings.shouldThreadComments = settingsObject.optBoolean(THREAD_COMMENTS_KEY, false); + mRemoteSettings.threadingLevels = settingsObject.optInt(THREAD_COMMENTS_DEPTH_KEY, 0); + mRemoteSettings.shouldPageComments = settingsObject.optBoolean(PAGE_COMMENTS_KEY, false); + mRemoteSettings.commentsPerPage = settingsObject.optInt(PAGE_COMMENT_COUNT_KEY, 0); + mRemoteSettings.commentApprovalRequired = settingsObject.optBoolean(COMMENT_MODERATION_KEY, false); + mRemoteSettings.commentsRequireIdentity = settingsObject.optBoolean(REQUIRE_IDENTITY_KEY, false); + mRemoteSettings.commentsRequireUserAccount = settingsObject.optBoolean(REQUIRE_USER_ACCOUNT_KEY, true); + mRemoteSettings.commentAutoApprovalKnownUsers = settingsObject.optBoolean(WHITELIST_KNOWN_USERS_KEY, false); + mRemoteSettings.maxLinks = settingsObject.optInt(MAX_LINKS_KEY, 0); + mRemoteSettings.holdForModeration = new ArrayList<>(); + mRemoteSettings.blacklist = new ArrayList<>(); + + String modKeys = settingsObject.optString(MODERATION_KEYS_KEY, ""); + if (modKeys.length() > 0) { + Collections.addAll(mRemoteSettings.holdForModeration, modKeys.split("\n")); + } + String blacklistKeys = settingsObject.optString(BLACKLIST_KEYS_KEY, ""); + if (blacklistKeys.length() > 0) { + Collections.addAll(mRemoteSettings.blacklist, blacklistKeys.split("\n")); + } + + if (settingsObject.optString(COMMENT_SORT_ORDER_KEY, "").equals("asc")) { + mRemoteSettings.sortCommentsBy = ASCENDING_SORT; + } else { + mRemoteSettings.sortCommentsBy = DESCENDING_SORT; + } + + if (settingsObject.optBoolean(RELATED_POSTS_ALLOWED_KEY, false)) { + mRemoteSettings.showRelatedPosts = settingsObject.optBoolean(RELATED_POSTS_ENABLED_KEY, false); + mRemoteSettings.showRelatedPostHeader = settingsObject.optBoolean(RELATED_POSTS_HEADER_KEY, false); + mRemoteSettings.showRelatedPostImages = settingsObject.optBoolean(RELATED_POSTS_IMAGES_KEY, false); + } + } + + /** + * Helper method to create the parameters for the site settings POST request + * + * Using undocumented endpoint WPCOM_JSON_API_Site_Settings_Endpoint + * https://wpcom.trac.automattic.com/browser/trunk/public.api/rest/json-endpoints.php#L1903 + */ + public Map serializeDotComParams() { + Map params = new HashMap<>(); + + if (mSettings.title!= null && !mSettings.title.equals(mRemoteSettings.title)) { + params.put(SET_TITLE_KEY, mSettings.title); + } + if (mSettings.tagline != null && !mSettings.tagline.equals(mRemoteSettings.tagline)) { + params.put(SET_DESC_KEY, mSettings.tagline); + } + if (mSettings.languageId != mRemoteSettings.languageId) { + params.put(LANGUAGE_ID_KEY, String.valueOf((mSettings.languageId))); + } + if (mSettings.privacy != mRemoteSettings.privacy) { + params.put(PRIVACY_KEY, String.valueOf((mSettings.privacy))); + } + if (mSettings.defaultCategory != mRemoteSettings.defaultCategory) { + params.put(DEF_CATEGORY_KEY, String.valueOf(mSettings.defaultCategory)); + } + if (mSettings.defaultPostFormat != null && !mSettings.defaultPostFormat.equals(mRemoteSettings.defaultPostFormat)) { + params.put(DEF_POST_FORMAT_KEY, mSettings.defaultPostFormat); + } + if (mSettings.showRelatedPosts != mRemoteSettings.showRelatedPosts || + mSettings.showRelatedPostHeader != mRemoteSettings.showRelatedPostHeader || + mSettings.showRelatedPostImages != mRemoteSettings.showRelatedPostImages) { + params.put(RELATED_POSTS_ENABLED_KEY, String.valueOf(mSettings.showRelatedPosts)); + params.put(RELATED_POSTS_HEADER_KEY, String.valueOf(mSettings.showRelatedPostHeader)); + params.put(RELATED_POSTS_IMAGES_KEY, String.valueOf(mSettings.showRelatedPostImages)); + } + if (mSettings.allowComments != mRemoteSettings.allowComments) { + params.put(ALLOW_COMMENTS_KEY, String.valueOf(mSettings.allowComments)); + } + if (mSettings.sendPingbacks != mRemoteSettings.sendPingbacks) { + params.put(SEND_PINGBACKS_KEY, String.valueOf(mSettings.sendPingbacks)); + } + if (mSettings.receivePingbacks != mRemoteSettings.receivePingbacks) { + params.put(RECEIVE_PINGBACKS_KEY, String.valueOf(mSettings.receivePingbacks)); + } + if (mSettings.commentApprovalRequired != mRemoteSettings.commentApprovalRequired) { + params.put(COMMENT_MODERATION_KEY, String.valueOf(mSettings.commentApprovalRequired)); + } + if (mSettings.closeCommentAfter != mRemoteSettings.closeCommentAfter + || mSettings.shouldCloseAfter != mRemoteSettings.shouldCloseAfter) { + params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(mSettings.shouldCloseAfter)); + params.put(CLOSE_OLD_COMMENTS_DAYS_KEY, String.valueOf(mSettings.closeCommentAfter)); + } + if (mSettings.sortCommentsBy != mRemoteSettings.sortCommentsBy) { + if (mSettings.sortCommentsBy == ASCENDING_SORT) { + params.put(COMMENT_SORT_ORDER_KEY, "asc"); + } else if (mSettings.sortCommentsBy == DESCENDING_SORT) { + params.put(COMMENT_SORT_ORDER_KEY, "desc"); + } + } + if (mSettings.threadingLevels != mRemoteSettings.threadingLevels + || mSettings.shouldThreadComments != mRemoteSettings.shouldThreadComments) { + params.put(THREAD_COMMENTS_KEY, String.valueOf(mSettings.shouldThreadComments)); + params.put(THREAD_COMMENTS_DEPTH_KEY, String.valueOf(mSettings.threadingLevels)); + } + if (mSettings.commentsPerPage != mRemoteSettings.commentsPerPage + || mSettings.shouldPageComments != mRemoteSettings.shouldPageComments) { + params.put(PAGE_COMMENTS_KEY, String.valueOf(mSettings.shouldPageComments)); + params.put(PAGE_COMMENT_COUNT_KEY, String.valueOf(mSettings.commentsPerPage)); + } + if (mSettings.commentsRequireIdentity != mRemoteSettings.commentsRequireIdentity) { + params.put(REQUIRE_IDENTITY_KEY, String.valueOf(mSettings.commentsRequireIdentity)); + } + if (mSettings.commentsRequireUserAccount != mRemoteSettings.commentsRequireUserAccount) { + params.put(REQUIRE_USER_ACCOUNT_KEY, String.valueOf(mSettings.commentsRequireUserAccount)); + } + if (mSettings.commentAutoApprovalKnownUsers != mRemoteSettings.commentAutoApprovalKnownUsers) { + params.put(WHITELIST_KNOWN_USERS_KEY, String.valueOf(mSettings.commentAutoApprovalKnownUsers)); + } + if (mSettings.maxLinks != mRemoteSettings.maxLinks) { + params.put(MAX_LINKS_KEY, String.valueOf(mSettings.maxLinks)); + } + if (mSettings.holdForModeration != null && !mSettings.holdForModeration.equals(mRemoteSettings.holdForModeration)) { + StringBuilder builder = new StringBuilder(); + for (String key : mSettings.holdForModeration) { + builder.append(key); + builder.append("\n"); + } + if (builder.length() > 1) { + params.put(MODERATION_KEYS_KEY, builder.substring(0, builder.length() - 1)); + } else { + params.put(MODERATION_KEYS_KEY, ""); + } + } + if (mSettings.blacklist != null && !mSettings.blacklist.equals(mRemoteSettings.blacklist)) { + StringBuilder builder = new StringBuilder(); + for (String key : mSettings.blacklist) { + builder.append(key); + builder.append("\n"); + } + if (builder.length() > 1) { + params.put(BLACKLIST_KEYS_KEY, builder.substring(0, builder.length() - 1)); + } else { + params.put(BLACKLIST_KEYS_KEY, ""); + } + } + + return params; + } + + /** + * Request a list of post categories for a site via the WordPress REST API. + */ + private void fetchCategories() { + WordPress.getRestClientUtilsV1_1().getCategories(String.valueOf(mBlog.getRemoteBlogId()), + new RestRequest.Listener() { + @Override + public void onResponse(JSONObject response) { + AppLog.d(AppLog.T.API, "Received response to Categories REST request."); + credentialsVerified(true); + + CategoryModel[] models = deserializeJsonRestResponse(response); + if (models == null) return; + + SiteSettingsTable.saveCategories(models); + mRemoteSettings.categories = models; + mSettings.categories = models; + notifyUpdatedOnUiThread(null); + } + }, new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + AppLog.d(AppLog.T.API, "Error fetching WP.com categories:" + error); + } + }); + } + + private CategoryModel deserializeCategoryFromJson(JSONObject category) throws JSONException { + if (category == null) return null; + + CategoryModel model = new CategoryModel(); + model.id = category.getInt(CAT_ID_KEY); + model.name = category.getString(CAT_NAME_KEY); + model.slug = category.getString(CAT_SLUG_KEY); + model.description = category.getString(CAT_DESC_KEY); + model.parentId = category.getInt(CAT_PARENT_ID_KEY); + model.postCount = category.getInt(CAT_POST_COUNT_KEY); + + return model; + } + + private CategoryModel[] deserializeJsonRestResponse(JSONObject response) { + try { + int num = response.getInt(CAT_NUM_POSTS_KEY); + JSONArray categories = response.getJSONArray(CATEGORIES_KEY); + CategoryModel[] models = new CategoryModel[num]; + + for (int i = 0; i < num; ++i) { + JSONObject category = categories.getJSONObject(i); + models[i] = deserializeCategoryFromJson(category); + } + + AppLog.d(AppLog.T.API, "Successfully fetched WP.com categories"); + + return models; + } catch (JSONException exception) { + AppLog.d(AppLog.T.API, "Error parsing WP.com categories response:" + response); + return null; + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java new file mode 100644 index 000000000000..e9a98e049ad6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java @@ -0,0 +1,88 @@ +package org.wordpress.android.ui.prefs; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.preference.Preference; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import org.wordpress.android.R; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.util.AnalyticsUtils; + +public class LearnMorePreference extends Preference + implements PreferenceHint, View.OnClickListener, DialogInterface.OnDismissListener { + private static final String WP_SUPPORT_URL = "https://en.support.wordpress.com/settings/discussion-settings/#default-article-settings"; + + private String mHint; + private Dialog mDialog; + + public LearnMorePreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected View onCreateView(@NonNull ViewGroup parent) { + super.onCreateView(parent); + + View view = View.inflate(getContext(), R.layout.learn_more_pref, null); + view.findViewById(R.id.learn_more_button).setOnClickListener(this); + + return view; + } + + @Override + public void onClick(View v) { + if (mDialog != null) return; + + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_LEARN_MORE_CLICKED); + + Context context = getContext(); + mDialog = new Dialog(context); + mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + mDialog.setOnDismissListener(this); + mDialog.setContentView(R.layout.learn_more_pref_screen); + WebView webView = new WebView(context); + webView.setWebViewClient(new WebViewClient() { + @Override + public void onPageFinished(WebView webView, String url) { + super.onPageFinished(webView, url); + if (mDialog != null) { + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_LEARN_MORE_LOADED); + mDialog.setContentView(webView); + } + } + }); + webView.loadUrl(WP_SUPPORT_URL); + mDialog.show(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + mDialog = null; + } + + @Override + public boolean hasHint() { + return !TextUtils.isEmpty(mHint); + } + + @Override + public String getHint() { + return mHint; + } + + @Override + public void setHint(String hint) { + mHint = hint; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectListView.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectListView.java new file mode 100644 index 000000000000..5c03e54aab3e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectListView.java @@ -0,0 +1,122 @@ +package org.wordpress.android.ui.prefs; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; + +import org.wordpress.android.R; + +/** + * ListView that supports multiple item selection and provides a delete button. + */ +public class MultiSelectListView extends ListView + implements AdapterView.OnItemLongClickListener, + AdapterView.OnItemClickListener, + ActionMode.Callback { + + public interface OnEnterMultiSelect { + void onEnterMultiSelect(); + } + + public interface OnExitMultiSelect { + void onExitMultiSelect(); + } + + public interface OnDeleteRequested { + /** + * @return + * true to exit Action Mode + */ + boolean onDeleteRequested(); + } + + private OnEnterMultiSelect mEnterListener; + private OnExitMultiSelect mExitListener; + private OnDeleteRequested mDeleteListener; + private ActionMode mActionMode; + + public MultiSelectListView(Context context, AttributeSet attrs) { + super(context, attrs); + setOnItemClickListener(this); + setOnItemLongClickListener(this); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (mActionMode == null) return; + + if (getCheckedItemCount() <= 0) { + mActionMode.finish(); + } else { + int color = isItemChecked(position) ? R.color.white : R.color.transparent; + getChildAt(position).setBackgroundColor(getResources().getColor(color)); + mActionMode.invalidate(); + } + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (mActionMode != null) return false; + + setItemChecked(position, true); + getChildAt(position).setBackgroundColor(getResources().getColor(R.color.white)); + mActionMode = startActionMode(this); + if (mEnterListener != null) mEnterListener.onEnterMultiSelect(); + + return true; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.list_editor, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + mode.setTitle(String.valueOf(getCheckedItemCount())); + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == R.id.menu_delete) { + if (mDeleteListener == null || mDeleteListener.onDeleteRequested()) { + mActionMode.finish(); + } + return true; + } + + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + for (int i = 0; i < getChildCount(); ++i) { + getChildAt(i).setBackgroundColor(getResources().getColor(R.color.transparent)); + } + + clearChoices(); + mActionMode = null; + if (mExitListener != null) mExitListener.onExitMultiSelect(); + } + + public void setEnterMultiSelectListener(OnEnterMultiSelect listener) { + mEnterListener = listener; + } + + public void setExitMultiSelectListener(OnExitMultiSelect listener) { + mExitListener = listener; + } + + public void setDeleteRequestListener(OnDeleteRequested listener) { + mDeleteListener = listener; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java new file mode 100644 index 000000000000..cc2977624aed --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java @@ -0,0 +1,159 @@ +package org.wordpress.android.ui.prefs; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.widget.SwitchCompat; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.NumberPicker; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.util.WPPrefUtils; + +public class NumberPickerDialog extends DialogFragment + implements DialogInterface.OnClickListener, + CompoundButton.OnCheckedChangeListener { + + public static final String SHOW_SWITCH_KEY = "show-switch"; + public static final String SWITCH_ENABLED_KEY = "switch-enabled"; + public static final String SWITCH_TITLE_KEY = "switch-title"; + public static final String TITLE_KEY = "dialog-title"; + public static final String HEADER_TEXT_KEY = "header-text"; + public static final String MIN_VALUE_KEY = "min-value"; + public static final String MAX_VALUE_KEY = "max-value"; + public static final String CUR_VALUE_KEY = "cur-value"; + + private static final int DEFAULT_MIN_VALUE = 0; + private static final int DEFAULT_MAX_VALUE = 99; + + private SwitchCompat mSwitch; + private TextView mHeaderText; + private NumberPicker mNumberPicker; + private int mMinValue; + private int mMaxValue; + private boolean mConfirmed; + + public NumberPickerDialog() { + mMinValue = DEFAULT_MIN_VALUE; + mMaxValue = DEFAULT_MAX_VALUE; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog); + View view = View.inflate(getActivity(), R.layout.number_picker_dialog, null); + TextView switchText = (TextView) view.findViewById(R.id.number_picker_text); + mSwitch = (SwitchCompat) view.findViewById(R.id.number_picker_switch); + mHeaderText = (TextView) view.findViewById(R.id.number_picker_header); + mNumberPicker = (NumberPicker) view.findViewById(R.id.number_picker); + int value = mMinValue; + + Bundle args = getArguments(); + if (args != null) { + if (args.getBoolean(SHOW_SWITCH_KEY, false)) { + mSwitch.setVisibility(View.VISIBLE); + mSwitch.setText(args.getString(SWITCH_TITLE_KEY, "")); + mSwitch.setChecked(args.getBoolean(SWITCH_ENABLED_KEY, false)); + final View toggleContainer = view.findViewById(R.id.number_picker_toggleable); + toggleContainer.setEnabled(mSwitch.isChecked()); + mNumberPicker.setEnabled(mSwitch.isChecked()); + } else { + mSwitch.setVisibility(View.GONE); + } + switchText.setText(args.getString(SWITCH_TITLE_KEY, "")); + mHeaderText.setText(args.getString(HEADER_TEXT_KEY, "")); + mMinValue = args.getInt(MIN_VALUE_KEY, DEFAULT_MIN_VALUE); + mMaxValue = args.getInt(MAX_VALUE_KEY, DEFAULT_MAX_VALUE); + value = args.getInt(CUR_VALUE_KEY, mMinValue); + + builder.setCustomTitle(getDialogTitleView(args.getString(TITLE_KEY, ""))); + } + + mNumberPicker.setMinValue(mMinValue); + mNumberPicker.setMaxValue(mMaxValue); + mNumberPicker.setValue(value); + + mSwitch.setOnCheckedChangeListener(this); + + // hide empty text views + if (TextUtils.isEmpty(switchText.getText())) { + switchText.setVisibility(View.GONE); + } + if (TextUtils.isEmpty(mHeaderText.getText())) { + mHeaderText.setVisibility(View.GONE); + } + + builder.setPositiveButton(R.string.ok, this); + builder.setNegativeButton(R.string.cancel, this); + builder.setView(view); + + return builder.create(); + } + + @Override + public void onStart() { + super.onStart(); + + AlertDialog dialog = (AlertDialog) getDialog(); + Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + if (positive != null) WPPrefUtils.layoutAsFlatButton(positive); + if (negative != null) WPPrefUtils.layoutAsFlatButton(negative); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + mConfirmed = which == DialogInterface.BUTTON_POSITIVE; + dismiss(); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mNumberPicker.setEnabled(isChecked); + mHeaderText.setEnabled(isChecked); + } + + @Override + public void onDismiss(DialogInterface dialog) { + Fragment target = getTargetFragment(); + if (target != null) { + target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getResultIntent()); + } + + super.onDismiss(dialog); + } + + private View getDialogTitleView(String title) { + LayoutInflater inflater = LayoutInflater.from(getActivity()); + @SuppressLint("InflateParams") + View titleView = inflater.inflate(R.layout.detail_list_preference_title, null); + TextView titleText = ((TextView) titleView.findViewById(R.id.title)); + titleText.setText(title); + titleText.setLayoutParams(new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.WRAP_CONTENT)); + return titleView; + } + + private Intent getResultIntent() { + if (mConfirmed) { + return new Intent() + .putExtra(SWITCH_ENABLED_KEY, mSwitch.isChecked()) + .putExtra(CUR_VALUE_KEY, mNumberPicker.getValue()); + } + + return null; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java new file mode 100644 index 000000000000..5047ad17e356 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java @@ -0,0 +1,7 @@ +package org.wordpress.android.ui.prefs; + +public interface PreferenceHint { + boolean hasHint(); + String getHint(); + void setHint(String hint); +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java new file mode 100644 index 000000000000..446545bde4b9 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java @@ -0,0 +1,184 @@ +package org.wordpress.android.ui.prefs; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.util.WPPrefUtils; +import org.wordpress.android.widgets.WPSwitch; + +import java.util.ArrayList; +import java.util.List; + +public class RelatedPostsDialog extends DialogFragment + implements DialogInterface.OnClickListener, + CompoundButton.OnCheckedChangeListener { + + /** + * boolean + * + * Sets the default state of the Show Related Posts switch. The switch is off by default. + */ + public static final String SHOW_RELATED_POSTS_KEY = "related-posts"; + + /** + * boolean + * + * Sets the default state of the Show Headers checkbox. The checkbox is off by default. + */ + public static final String SHOW_HEADER_KEY = "show-header"; + + /** + * boolean + * + * Sets the default state of the Show Images checkbox. The checkbox is off by default. + */ + public static final String SHOW_IMAGES_KEY = "show-images"; + + private WPSwitch mShowRelatedPosts; + private CheckBox mShowHeader; + private CheckBox mShowImages; + private TextView mPreviewHeader; + private TextView mRelatedPostsListHeader; + private LinearLayout mRelatedPostsList; + private List mPreviewImages; + private boolean mConfirmed; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + LayoutInflater inflater = getActivity().getLayoutInflater(); + View v = inflater.inflate(R.layout.related_posts_dialog, null, false); + + mShowRelatedPosts = (WPSwitch) v.findViewById(R.id.toggle_related_posts_switch); + mShowHeader = (CheckBox) v.findViewById(R.id.show_header_checkbox); + mShowImages = (CheckBox) v.findViewById(R.id.show_images_checkbox); + mPreviewHeader = (TextView) v.findViewById(R.id.preview_header); + mRelatedPostsListHeader = (TextView) v.findViewById(R.id.related_posts_list_header); + mRelatedPostsList = (LinearLayout) v.findViewById(R.id.related_posts_list); + + mPreviewImages = new ArrayList<>(); + mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image1)); + mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image2)); + mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image3)); + + Bundle args = getArguments(); + if (args != null) { + mShowRelatedPosts.setChecked(args.getBoolean(SHOW_RELATED_POSTS_KEY)); + mShowHeader.setChecked(args.getBoolean(SHOW_HEADER_KEY)); + mShowImages.setChecked(args.getBoolean(SHOW_IMAGES_KEY)); + } + + toggleShowHeader(mShowHeader.isChecked()); + toggleShowImages(mShowImages.isChecked()); + + mShowRelatedPosts.setOnCheckedChangeListener(this); + mShowHeader.setOnCheckedChangeListener(this); + mShowImages.setOnCheckedChangeListener(this); + + toggleViews(mShowRelatedPosts.isChecked()); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog); + View titleView = inflater.inflate(R.layout.detail_list_preference_title, null); + TextView titleText = ((TextView) titleView.findViewById(R.id.title)); + titleText.setText(R.string.site_settings_related_posts_title); + titleText.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT)); + builder.setCustomTitle(titleView); + builder.setPositiveButton(R.string.ok, this); + builder.setNegativeButton(R.string.cancel, this); + builder.setView(v); + + return builder.create(); + } + + @Override + public void onStart() { + super.onStart(); + + AlertDialog dialog = (AlertDialog) getDialog(); + Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + if (positive != null) WPPrefUtils.layoutAsFlatButton(positive); + if (negative != null) WPPrefUtils.layoutAsFlatButton(negative); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + mConfirmed = which == DialogInterface.BUTTON_POSITIVE; + dismiss(); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView == mShowRelatedPosts) { + toggleViews(isChecked); + } else if (buttonView == mShowHeader) { + toggleShowHeader(isChecked); + } else if (buttonView == mShowImages) { + toggleShowImages(isChecked); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + Fragment target = getTargetFragment(); + if (target != null) { + target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getResultIntent()); + } + + super.onDismiss(dialog); + } + + private void toggleShowHeader(boolean show) { + if (show) { + mRelatedPostsListHeader.setVisibility(View.VISIBLE); + } else { + mRelatedPostsListHeader.setVisibility(View.GONE); + } + } + + private void toggleShowImages(boolean show) { + int visibility = show ? View.VISIBLE : View.GONE; + for (ImageView view : mPreviewImages) { + view.setVisibility(visibility); + } + } + + private Intent getResultIntent() { + if (mConfirmed) { + return new Intent() + .putExtra(SHOW_RELATED_POSTS_KEY, mShowRelatedPosts.isChecked()) + .putExtra(SHOW_HEADER_KEY, mShowHeader.isChecked()) + .putExtra(SHOW_IMAGES_KEY, mShowImages.isChecked()); + } + + return null; + } + + private void toggleViews(boolean enabled) { + mShowHeader.setEnabled(enabled); + mShowImages.setEnabled(enabled); + mPreviewHeader.setEnabled(enabled); + mRelatedPostsListHeader.setEnabled(enabled); + + if (enabled) { + mRelatedPostsList.setAlpha(1.0f); + } else { + mRelatedPostsList.setAlpha(0.5f); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java new file mode 100644 index 000000000000..932500ab9c79 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java @@ -0,0 +1,416 @@ +package org.wordpress.android.ui.prefs; + +import android.app.Activity; +import android.text.TextUtils; + +import org.wordpress.android.R; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.datasets.SiteSettingsTable; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.CategoryModel; +import org.wordpress.android.models.SiteSettingsModel; +import org.wordpress.android.util.AnalyticsUtils; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.MapUtils; +import org.xmlrpc.android.ApiHelper; +import org.xmlrpc.android.XMLRPCCallback; +import org.xmlrpc.android.XMLRPCClientInterface; +import org.xmlrpc.android.XMLRPCException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +class SelfHostedSiteSettings extends SiteSettingsInterface { + // XML-RPC wp.getOptions keys + public static final String PRIVACY_KEY = "blog_public"; + public static final String DEF_CATEGORY_KEY = "default_category"; + public static final String DEF_POST_FORMAT_KEY = "default_post_format"; + public static final String ALLOW_COMMENTS_KEY = "default_comment_status"; + public static final String SEND_PINGBACKS_KEY = "default_pingback_flag"; + public static final String RECEIVE_PINGBACKS_KEY = "default_ping_status"; + public static final String CLOSE_OLD_COMMENTS_KEY = "close_comments_for_old_posts"; + public static final String CLOSE_OLD_COMMENTS_DAYS_KEY = "close_comments_days_old"; + public static final String THREAD_COMMENTS_KEY = "thread_comments"; + public static final String THREAD_COMMENTS_DEPTH_KEY = "thread_comments_depth"; + public static final String PAGE_COMMENTS_KEY = "page_comments"; + public static final String PAGE_COMMENT_COUNT_KEY = "comments_per_page"; + public static final String COMMENT_SORT_ORDER_KEY = "comment_order"; + public static final String COMMENT_MODERATION_KEY = "comment_moderation"; + public static final String REQUIRE_IDENTITY_KEY = "require_name_email"; + public static final String REQUIRE_USER_ACCOUNT_KEY = "comment_registration"; + public static final String WHITELIST_KNOWN_USERS_KEY = "comment_whitelist"; + public static final String MAX_LINKS_KEY = "comment_max_links"; + public static final String MODERATION_KEYS_KEY = "moderation_keys"; + public static final String BLACKLIST_KEYS_KEY = "blacklist_keys"; + public static final String SOFTWARE_VERSION_KEY = "software_version"; + + private static final String BLOG_URL_KEY = "blog_url"; + private static final String BLOG_TITLE_KEY = "blog_title"; + private static final String BLOG_USERNAME_KEY = "username"; + private static final String BLOG_PASSWORD_KEY = "password"; + private static final String BLOG_TAGLINE_KEY = "blog_tagline"; + private static final String BLOG_CATEGORY_ID_KEY = "categoryId"; + private static final String BLOG_CATEGORY_PARENT_ID_KEY = "parentId"; + private static final String BLOG_CATEGORY_DESCRIPTION_KEY = "categoryDescription"; + private static final String BLOG_CATEGORY_NAME_KEY = "categoryName"; + + // Requires WordPress 4.5.x or higher + private static final int REQUIRED_MAJOR_VERSION = 4; + private static final int REQUIRED_MINOR_VERSION = 3; + + private static final String OPTION_ALLOWED = "open"; + private static final String OPTION_DISALLOWED = "closed"; + + SelfHostedSiteSettings(Activity host, Blog blog, SiteSettingsListener listener) { + super(host, blog, listener); + } + + @Override + public SiteSettingsInterface init(boolean fetch) { + super.init(fetch); + + if (mSettings.defaultCategory == 0) { + mSettings.defaultCategory = siteSettingsPreferences(mActivity).getInt(DEF_CATEGORY_PREF_KEY, 0); + } + if (TextUtils.isEmpty(mSettings.defaultPostFormat) || mSettings.defaultPostFormat.equals("0")) { + mSettings.defaultPostFormat = siteSettingsPreferences(mActivity).getString(DEF_FORMAT_PREF_KEY, "0"); + } + mSettings.language = siteSettingsPreferences(mActivity).getString(LANGUAGE_PREF_KEY, Locale.getDefault().getLanguage()); + + return this; + } + + @Override + public void saveSettings() { + super.saveSettings(); + + final Map params = serializeSelfHostedParams(); + if (params == null || params.isEmpty()) return; + + XMLRPCCallback callback = new XMLRPCCallback() { + @Override + public void onSuccess(long id, final Object result) { + notifySavedOnUiThread(null); + mRemoteSettings.copyFrom(mSettings); + + if (result != null) { + HashMap properties = new HashMap<>(); + if (result instanceof Map) { + Map resultMap = (Map) result; + Set keys = resultMap.keySet(); + for (String key : keys) { + Object currentValue = resultMap.get(key); + if (currentValue != null) { + properties.put(SAVED_ITEM_PREFIX + key, currentValue); + } + } + } + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_SAVED_REMOTELY, properties); + } + } + + @Override + public void onFailure(long id, final Exception error) { + notifySavedOnUiThread(error); + } + }; + final Object[] callParams = { + mBlog.getRemoteBlogId(), mSettings.username, mSettings.password, params + }; + + XMLRPCClientInterface xmlrpcInterface = instantiateInterface(); + if (xmlrpcInterface == null) return; + xmlrpcInterface.callAsync(callback, ApiHelper.Methods.SET_OPTIONS, callParams); + } + + /** + * Request remote site data via XML-RPC. + */ + @Override + protected void fetchRemoteData() { + + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + Object[] params = {mBlog.getRemoteBlogId(), mBlog.getUsername(), mBlog.getPassword()}; + + // Need two interfaces or the first call gets aborted + instantiateInterface().callAsync(mOptionsCallback, ApiHelper.Methods.GET_OPTIONS, params); + instantiateInterface().callAsync(mCategoriesCallback, ApiHelper.Methods.GET_CATEGORIES, params); + } + }); + thread.run(); + } + + /** + * Handles response to fetching self-hosted site categories via XML-RPC. + */ + private final XMLRPCCallback mCategoriesCallback = new XMLRPCCallback() { + @Override + public void onSuccess(long id, Object result) { + if (result instanceof Object[]) { + AppLog.d(AppLog.T.API, "Received Categories XML-RPC response."); + credentialsVerified(true); + + mRemoteSettings.localTableId = mBlog.getRemoteBlogId(); + deserializeCategoriesResponse(mRemoteSettings, (Object[]) result); + mSettings.categories = mRemoteSettings.categories; + SiteSettingsTable.saveCategories(mSettings.categories); + notifyUpdatedOnUiThread(null); + } else { + // Response is considered an error if we are unable to parse it + AppLog.w(AppLog.T.API, "Error parsing Categories XML-RPC response: " + result); + notifyUpdatedOnUiThread(new XMLRPCException("Unknown response object")); + } + } + + @Override + public void onFailure(long id, Exception error) { + AppLog.w(AppLog.T.API, "Error Categories XML-RPC response: " + error); + notifyUpdatedOnUiThread(error); + } + }; + + /** + * Handles response to fetching self-hosted site options via XML-RPC. + */ + private final XMLRPCCallback mOptionsCallback = new XMLRPCCallback() { + @Override + public void onSuccess(long id, final Object result) { + if (result instanceof Map) { + AppLog.d(AppLog.T.API, "Received Options XML-RPC response."); + + if (!versionSupported((Map) result) && mActivity != null) { + notifyUpdatedOnUiThread(new XMLRPCException(mActivity.getString(R.string.site_settings_unsupported_version_error))); + return; + } + + credentialsVerified(true); + + deserializeOptionsResponse(mRemoteSettings, (Map) result); + mSettings.copyFrom(mRemoteSettings); + SiteSettingsTable.saveSettings(mSettings); + notifyUpdatedOnUiThread(null); + } else { + // Response is considered an error if we are unable to parse it + AppLog.w(AppLog.T.API, "Error parsing Options XML-RPC response: " + result); + notifyUpdatedOnUiThread(new XMLRPCException("Unknown response object")); + } + } + + @Override + public void onFailure(long id, final Exception error) { + AppLog.w(AppLog.T.API, "Error Options XML-RPC response: " + error); + notifyUpdatedOnUiThread(error); + } + }; + + private boolean versionSupported(Map map) { + String version = getNestedMapValue(map, SOFTWARE_VERSION_KEY); + if (TextUtils.isEmpty(version)) return false; + String[] split = version.split("\\."); + return split.length > 0 && + Integer.valueOf(split[0]) >= REQUIRED_MAJOR_VERSION && + Integer.valueOf(split[1]) >= REQUIRED_MINOR_VERSION; + } + + private Map serializeSelfHostedParams() { + Map params = new HashMap<>(); + + if (mSettings.title != null && !mSettings.title.equals(mRemoteSettings.title)) { + params.put(BLOG_TITLE_KEY, mSettings.title); + } + if (mSettings.tagline != null && !mSettings.tagline.equals(mRemoteSettings.tagline)) { + params.put(BLOG_TAGLINE_KEY, mSettings.tagline); + } + if (mSettings.privacy != mRemoteSettings.privacy) { + params.put(PRIVACY_KEY, String.valueOf(mSettings.privacy)); + } + if (mSettings.defaultCategory != mRemoteSettings.defaultCategory) { + params.put(DEF_CATEGORY_KEY, String.valueOf(mSettings.defaultCategory)); + } + if (mSettings.defaultPostFormat != null && !mSettings.defaultPostFormat.equals(mRemoteSettings.defaultPostFormat)) { + params.put(DEF_POST_FORMAT_KEY, mSettings.defaultPostFormat); + } + if (mSettings.allowComments != mRemoteSettings.allowComments) { + params.put(ALLOW_COMMENTS_KEY, String.valueOf(mSettings.allowComments)); + } + if (mSettings.sendPingbacks != mRemoteSettings.sendPingbacks) { + params.put(SEND_PINGBACKS_KEY, mSettings.sendPingbacks ? "1" : "0"); + } + if (mSettings.receivePingbacks != mRemoteSettings.receivePingbacks) { + params.put(RECEIVE_PINGBACKS_KEY, mSettings.receivePingbacks ? OPTION_ALLOWED : OPTION_DISALLOWED); + } + if (mSettings.commentApprovalRequired != mRemoteSettings.commentApprovalRequired) { + params.put(COMMENT_MODERATION_KEY, String.valueOf(mSettings.commentApprovalRequired)); + } + if (mSettings.closeCommentAfter != mRemoteSettings.closeCommentAfter) { + if (mSettings.closeCommentAfter <= 0) { + params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(0)); + } else { + params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(1)); + params.put(CLOSE_OLD_COMMENTS_DAYS_KEY, String.valueOf(mSettings.closeCommentAfter)); + } + } + if (mSettings.sortCommentsBy != mRemoteSettings.sortCommentsBy) { + if (mSettings.sortCommentsBy == ASCENDING_SORT) { + params.put(COMMENT_SORT_ORDER_KEY, "asc"); + } else if (mSettings.sortCommentsBy == DESCENDING_SORT) { + params.put(COMMENT_SORT_ORDER_KEY, "desc"); + } + } + if (mSettings.threadingLevels != mRemoteSettings.threadingLevels) { + if (mSettings.threadingLevels <= 1) { + params.put(THREAD_COMMENTS_KEY, String.valueOf(0)); + } else { + params.put(PAGE_COMMENTS_KEY, String.valueOf(1)); + params.put(THREAD_COMMENTS_DEPTH_KEY, String.valueOf(mSettings.threadingLevels)); + } + } + if (mSettings.commentsPerPage != mRemoteSettings.commentsPerPage) { + if (mSettings.commentsPerPage <= 0) { + params.put(PAGE_COMMENTS_KEY, String.valueOf(0)); + } else{ + params.put(PAGE_COMMENTS_KEY, String.valueOf(1)); + params.put(PAGE_COMMENT_COUNT_KEY, String.valueOf(mSettings.commentsPerPage)); + } + } + if (mSettings.commentsRequireIdentity != mRemoteSettings.commentsRequireIdentity) { + params.put(REQUIRE_IDENTITY_KEY, String.valueOf(mSettings.commentsRequireIdentity ? 1 : 0)); + } + if (mSettings.commentsRequireUserAccount != mRemoteSettings.commentsRequireUserAccount) { + params.put(REQUIRE_USER_ACCOUNT_KEY, String.valueOf(mSettings.commentsRequireUserAccount ? 1 : 0)); + } + if (mSettings.commentAutoApprovalKnownUsers != mRemoteSettings.commentAutoApprovalKnownUsers) { + params.put(WHITELIST_KNOWN_USERS_KEY, String.valueOf(mSettings.commentAutoApprovalKnownUsers)); + } + if (mSettings.maxLinks != mRemoteSettings.maxLinks) { + params.put(MAX_LINKS_KEY, String.valueOf(mSettings.maxLinks)); + } + if (mSettings.holdForModeration != null && !mSettings.holdForModeration.equals(mRemoteSettings.holdForModeration)) { + StringBuilder builder = new StringBuilder(); + for (String key : mSettings.holdForModeration) { + builder.append(key); + builder.append("\n"); + } + if (builder.length() > 1) { + params.put(MODERATION_KEYS_KEY, builder.substring(0, builder.length() - 1)); + } else { + params.put(MODERATION_KEYS_KEY, ""); + } + } + if (mSettings.blacklist != null && !mSettings.blacklist.equals(mRemoteSettings.blacklist)) { + StringBuilder builder = new StringBuilder(); + for (String key : mSettings.blacklist) { + builder.append(key); + builder.append("\n"); + } + if (builder.length() > 1) { + params.put(BLACKLIST_KEYS_KEY, builder.substring(0, builder.length() - 1)); + } else { + params.put(BLACKLIST_KEYS_KEY, ""); + } + } + + return params; + } + + /** + * Sets values from a self-hosted XML-RPC response object. + */ + private void deserializeOptionsResponse(SiteSettingsModel model, Map response) { + if (mBlog == null || response == null) return; + + model.username = mBlog.getUsername(); + model.password = mBlog.getPassword(); + model.address = getNestedMapValue(response, BLOG_URL_KEY); + model.title = getNestedMapValue(response, BLOG_TITLE_KEY); + model.tagline = getNestedMapValue(response, BLOG_TAGLINE_KEY); + model.privacy = Integer.valueOf(getNestedMapValue(response, PRIVACY_KEY)); + model.defaultCategory = Integer.valueOf(getNestedMapValue(response, DEF_CATEGORY_KEY)); + model.defaultPostFormat = getNestedMapValue(response, DEF_POST_FORMAT_KEY); + model.allowComments = OPTION_ALLOWED.equals(getNestedMapValue(response, ALLOW_COMMENTS_KEY)); + model.receivePingbacks = OPTION_ALLOWED.equals(getNestedMapValue(response, RECEIVE_PINGBACKS_KEY)); + String sendPingbacks = getNestedMapValue(response, SEND_PINGBACKS_KEY); + String approvalRequired = getNestedMapValue(response, COMMENT_MODERATION_KEY); + String identityRequired = getNestedMapValue(response, REQUIRE_IDENTITY_KEY); + String accountRequired = getNestedMapValue(response, REQUIRE_USER_ACCOUNT_KEY); + String knownUsers = getNestedMapValue(response, WHITELIST_KNOWN_USERS_KEY); + model.sendPingbacks = !TextUtils.isEmpty(sendPingbacks) && Integer.valueOf(sendPingbacks) > 0; + model.commentApprovalRequired = !TextUtils.isEmpty(approvalRequired) && Boolean.valueOf(approvalRequired); + model.commentsRequireIdentity = !TextUtils.isEmpty(identityRequired) && Integer.valueOf(identityRequired) > 0; + model.commentsRequireUserAccount = !TextUtils.isEmpty(accountRequired) && Integer.valueOf(identityRequired) > 0; + model.commentAutoApprovalKnownUsers = !TextUtils.isEmpty(knownUsers) && Boolean.valueOf(knownUsers); + model.maxLinks = Integer.valueOf(getNestedMapValue(response, MAX_LINKS_KEY)); + mRemoteSettings.holdForModeration = new ArrayList<>(); + mRemoteSettings.blacklist = new ArrayList<>(); + + String modKeys = getNestedMapValue(response, MODERATION_KEYS_KEY); + if (modKeys.length() > 0) { + Collections.addAll(mRemoteSettings.holdForModeration, modKeys.split("\n")); + } + String blacklistKeys = getNestedMapValue(response, BLACKLIST_KEYS_KEY); + if (blacklistKeys.length() > 0) { + Collections.addAll(mRemoteSettings.blacklist, blacklistKeys.split("\n")); + } + + String close = getNestedMapValue(response, CLOSE_OLD_COMMENTS_KEY); + if (!TextUtils.isEmpty(close) && Boolean.valueOf(close)) { + mRemoteSettings.closeCommentAfter = Integer.valueOf(getNestedMapValue(response, CLOSE_OLD_COMMENTS_DAYS_KEY)); + } else { + mRemoteSettings.closeCommentAfter = 0; + } + + String thread = getNestedMapValue(response, THREAD_COMMENTS_KEY); + if (!TextUtils.isEmpty(thread) && Integer.valueOf(thread) > 0) { + mRemoteSettings.threadingLevels = Integer.valueOf(getNestedMapValue(response, THREAD_COMMENTS_DEPTH_KEY)); + } else { + mRemoteSettings.threadingLevels = 0; + } + + String page = getNestedMapValue(response, PAGE_COMMENTS_KEY); + if (!TextUtils.isEmpty(page) && Boolean.valueOf(page)) { + mRemoteSettings.commentsPerPage = Integer.valueOf(getNestedMapValue(response, PAGE_COMMENT_COUNT_KEY)); + } else { + mRemoteSettings.commentsPerPage = 0; + } + + if (getNestedMapValue(response, COMMENT_SORT_ORDER_KEY).equals("asc")) { + mRemoteSettings.sortCommentsBy = ASCENDING_SORT; + } else { + mRemoteSettings.sortCommentsBy = DESCENDING_SORT; + } + } + + private void deserializeCategoriesResponse(SiteSettingsModel model, Object[] response) { + model.categories = new CategoryModel[response.length]; + + for (int i = 0; i < response.length; ++i) { + if (response[i] instanceof Map) { + Map category = (Map) response[i]; + CategoryModel categoryModel = new CategoryModel(); + categoryModel.id = MapUtils.getMapInt(category, BLOG_CATEGORY_ID_KEY); + categoryModel.parentId = MapUtils.getMapInt(category, BLOG_CATEGORY_PARENT_ID_KEY); + categoryModel.description = MapUtils.getMapStr(category, BLOG_CATEGORY_DESCRIPTION_KEY); + categoryModel.name = MapUtils.getMapStr(category, BLOG_CATEGORY_NAME_KEY); + model.categories[i] = categoryModel; + } + } + } + + /** + * Helper method to get a value from a nested Map. Used to parse self-hosted response objects. + */ + private String getNestedMapValue(Map map, String key) { + if (map != null && key != null) { + return MapUtils.getMapStr((Map) map.get(key), "value"); + } + + return ""; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SettingsFragment.java index e1a6e3247c45..7c8881dc80be 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SettingsFragment.java @@ -22,22 +22,12 @@ import android.widget.ArrayAdapter; import android.widget.ListView; -import com.android.volley.VolleyError; -import com.wordpress.rest.RestRequest; - -import org.json.JSONObject; import org.wordpress.android.R; import org.wordpress.android.analytics.AnalyticsTracker; import org.wordpress.android.analytics.AnalyticsTracker.Stat; -import org.wordpress.android.models.AccountHelper; -import org.wordpress.android.ui.ActivityLauncher; import org.wordpress.android.ui.ShareIntentReceiverActivity; -import org.wordpress.android.ui.notifications.utils.NotificationsUtils; -import org.wordpress.android.ui.prefs.notifications.NotificationsSettingsActivity; import org.wordpress.android.util.ActivityUtils; import org.wordpress.android.util.AnalyticsUtils; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.widgets.WPEditTextPreference; @@ -207,7 +197,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) if (position != 0) { Map properties = new HashMap(); properties.put("forced_app_locale", conf.locale.toString()); - AnalyticsTracker.track(Stat.SETTINGS_LANGUAGE_SELECTION_FORCED, properties); + AnalyticsTracker.track(Stat.ACCOUNT_SETTINGS_LANGUAGE_SELECTION_FORCED, properties); } // Language is now part of metadata, so we need to refresh them diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java new file mode 100644 index 000000000000..f6306a18bd76 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java @@ -0,0 +1,1042 @@ +package org.wordpress.android.ui.prefs; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Handler; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.SparseBooleanArray; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.models.AccountHelper; +import org.wordpress.android.models.Blog; +import org.wordpress.android.ui.stats.StatsWidgetProvider; +import org.wordpress.android.ui.stats.datasets.StatsTable; +import org.wordpress.android.util.AnalyticsUtils; +import org.wordpress.android.util.CoreEvents; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.StringUtils; +import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.WPActivityUtils; +import org.wordpress.android.util.WPPrefUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import de.greenrobot.event.EventBus; + +/** + * Allows interfacing with WordPress site settings. Works with WP.com and WP.org v4.5+ (pending). + * + * Settings are synced automatically when local changes are made. + */ + +public class SiteSettingsFragment extends PreferenceFragment + implements Preference.OnPreferenceChangeListener, + Preference.OnPreferenceClickListener, + AdapterView.OnItemLongClickListener, + ViewGroup.OnHierarchyChangeListener, + Dialog.OnDismissListener, + SiteSettingsInterface.SiteSettingsListener { + + /** + * Use this argument to pass the {@link Integer} local blog ID to this fragment. + */ + public static final String ARG_LOCAL_BLOG_ID = "local_blog_id"; + + /** + * When the user removes a site (by selecting Delete Site) the parent {@link Activity} result + * is set to this value and {@link Activity#finish()} is invoked. + */ + public static final int RESULT_BLOG_REMOVED = Activity.RESULT_FIRST_USER; + + /** + * Provides the regex to identify domain HTTP(S) protocol and/or 'www' sub-domain. + * + * Used to format user-facing {@link String}'s in certain preferences. + */ + private static final String ADDRESS_FORMAT_REGEX = "^(https?://(w{3})?|www\\.)"; + + /** + * Used to move the Uncategorized category to the beginning of the category list. + */ + private static final int UNCATEGORIZED_CATEGORY_ID = 1; + + /** + * Request code used when creating the {@link RelatedPostsDialog}. + */ + private static final int RELATED_POSTS_REQUEST_CODE = 1; + + private static final int THREADING_REQUEST_CODE = 2; + private static final int PAGING_REQUEST_CODE = 3; + private static final int CLOSE_AFTER_REQUEST_CODE = 4; + private static final int MULTIPLE_LINKS_REQUEST_CODE = 5; + + private static final long FETCH_DELAY = 1000; + + // Reference to blog obtained from passed ID (ARG_LOCAL_BLOG_ID) + private Blog mBlog; + + // Can interface with WP.com or WP.org + private SiteSettingsInterface mSiteSettings; + + // Reference to the list of items being edited in the current list editor + private List mEditingList; + + // Used to ensure that settings are only fetched once throughout the lifecycle of the fragment + private boolean mShouldFetch; + + // General settings + private EditTextPreference mTitlePref; + private EditTextPreference mTaglinePref; + private EditTextPreference mAddressPref; + private DetailListPreference mPrivacyPref; + private DetailListPreference mLanguagePref; + + // Account settings (NOTE: only for WP.org) + private EditTextPreference mUsernamePref; + private EditTextPreference mPasswordPref; + + // Writing settings + private WPSwitchPreference mLocationPref; + private DetailListPreference mCategoryPref; + private DetailListPreference mFormatPref; + private Preference mRelatedPostsPref; + + // Discussion settings preview + private WPSwitchPreference mAllowCommentsPref; + private WPSwitchPreference mSendPingbacksPref; + private WPSwitchPreference mReceivePingbacksPref; + + // Discussion settings -> Defaults for New Posts + private WPSwitchPreference mAllowCommentsNested; + private WPSwitchPreference mSendPingbacksNested; + private WPSwitchPreference mReceivePingbacksNested; + + // Discussion settings -> Comments + private WPSwitchPreference mIdentityRequiredPreference; + private WPSwitchPreference mUserAccountRequiredPref; + private Preference mCloseAfterPref; + private DetailListPreference mSortByPref; + private DetailListPreference mThreadingPref; + private Preference mPagingPref; + private DetailListPreference mWhitelistPref; + private Preference mMultipleLinksPref; + private Preference mModerationHoldPref; + private Preference mBlacklistPref; + + // This Device settings + private DetailListPreference mImageWidthPref; + private WPSwitchPreference mUploadAndLinkPref; + + // Delete site option (NOTE: only for WP.org) + private Preference mDeleteSitePref; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Activity activity = getActivity(); + + // make sure we have local site data and a network connection, otherwise finish activity + mBlog = WordPress.getBlog(getArguments().getInt(ARG_LOCAL_BLOG_ID, -1)); + if (mBlog == null || !NetworkUtils.checkConnection(activity)) { + getActivity().finish(); + return; + } + + // track successful settings screen access + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_ACCESSED); + + // setup state to fetch remote settings + mShouldFetch = true; + + // initialize the appropriate settings interface (WP.com or WP.org) + mSiteSettings = SiteSettingsInterface.getInterface(activity, mBlog, this); + + setRetainInstance(true); + addPreferencesFromResource(R.xml.site_settings); + + // toggle which preferences are shown and set references + initPreferences(); + } + + @Override + public void onPause() { + super.onPause(); + WordPress.wpDB.saveBlog(mBlog); + } + + @Override + public void onResume() { + super.onResume(); + + // always load cached settings + mSiteSettings.init(false); + + if (mShouldFetch) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + // initialize settings with locally cached values, fetch remote on first pass + mSiteSettings.init(true); + } + }, FETCH_DELAY); + // stop future calls from fetching remote settings + mShouldFetch = false; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case RELATED_POSTS_REQUEST_CODE: + // data is null if user cancelled editing Related Posts settings + if (data == null) break; + mSiteSettings.setShowRelatedPosts(data.getBooleanExtra( + RelatedPostsDialog.SHOW_RELATED_POSTS_KEY, false)); + mSiteSettings.setShowRelatedPostHeader(data.getBooleanExtra( + RelatedPostsDialog.SHOW_HEADER_KEY, false)); + mSiteSettings.setShowRelatedPostImages(data.getBooleanExtra( + RelatedPostsDialog.SHOW_IMAGES_KEY, false)); + mSiteSettings.saveSettings(); + break; + case THREADING_REQUEST_CODE: + if (data == null) break; + mSiteSettings.setShouldThreadComments(data.getBooleanExtra + (NumberPickerDialog.SWITCH_ENABLED_KEY, false)); + onPreferenceChange(mThreadingPref, data.getIntExtra( + NumberPickerDialog.CUR_VALUE_KEY, -1)); + break; + case PAGING_REQUEST_CODE: + if (data == null) break; + mSiteSettings.setShouldPageComments(data.getBooleanExtra + (NumberPickerDialog.SWITCH_ENABLED_KEY, false)); + onPreferenceChange(mPagingPref, data.getIntExtra( + NumberPickerDialog.CUR_VALUE_KEY, -1)); + break; + case CLOSE_AFTER_REQUEST_CODE: + if (data == null) break; + mSiteSettings.setShouldCloseAfter(data.getBooleanExtra + (NumberPickerDialog.SWITCH_ENABLED_KEY, false)); + onPreferenceChange(mCloseAfterPref, data.getIntExtra( + NumberPickerDialog.CUR_VALUE_KEY, -1)); + break; + case MULTIPLE_LINKS_REQUEST_CODE: + if (data == null) break; + int numLinks = data.getIntExtra(NumberPickerDialog.CUR_VALUE_KEY, -1); + if (numLinks < 0 || numLinks == mSiteSettings.getMultipleLinks()) return; + onPreferenceChange(mMultipleLinksPref, numLinks); + break; + } + + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) { + // use a wrapper to apply the Calypso theme + Context themer = new ContextThemeWrapper(getActivity(), R.style.Calypso_SiteSettingsTheme); + LayoutInflater localInflater = inflater.cloneInContext(themer); + View view = super.onCreateView(localInflater, container, savedInstanceState); + + if (view != null) { + setupPreferenceList((ListView) view.findViewById(android.R.id.list), getResources()); + } + + return view; + } + + @Override + public void onChildViewAdded(View parent, View child) { + if (child.getId() == android.R.id.title && child instanceof TextView) { + // style preference category title views + TextView title = (TextView) child; + WPPrefUtils.layoutAsBody2(title); + } else { + // style preference title views + TextView title = (TextView) child.findViewById(android.R.id.title); + if (title != null) WPPrefUtils.layoutAsSubhead(title); + } + } + + @Override + public void onChildViewRemoved(View parent, View child) { + // NOP + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + super.onPreferenceTreeClick(screen, preference); + + // More preference selected, style the Discussion screen + if (preference == findPreference(getString(R.string.pref_key_site_more_discussion))) { + Dialog dialog = ((PreferenceScreen) preference).getDialog(); + if (dialog == null) return false; + + setupPreferenceList((ListView) dialog.findViewById(android.R.id.list), getResources()); + + // add Action Bar + String title = getString(R.string.site_settings_discussion_title); + WPActivityUtils.addToolbarToDialog(this, dialog, title); + + // track user accessing the full Discussion settings screen + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_ACCESSED_MORE_SETTINGS); + } + + return false; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference == mRelatedPostsPref) { + showRelatedPostsDialog(); + return true; + } else if (preference == mMultipleLinksPref) { + showMultipleLinksDialog(); + return true; + } else if (preference == mModerationHoldPref) { + mEditingList = mSiteSettings.getModerationKeys(); + showListEditorDialog(R.string.site_settings_moderation_hold_title, + R.string.site_settings_hold_for_moderation_description); + return true; + } else if (preference == mBlacklistPref) { + mEditingList = mSiteSettings.getBlacklistKeys(); + showListEditorDialog(R.string.site_settings_blacklist_title, + R.string.site_settings_blacklist_description); + return true; + } else if (preference == mDeleteSitePref) { + removeBlogWithConfirmation(); + return true; + } else if (preference == mCloseAfterPref) { + showCloseAfterDialog(); + return true; + } else if (preference == mPagingPref) { + showPagingDialog(); + return true; + } else if (preference == mCategoryPref || preference == mFormatPref) { + return !shouldShowListPreference((DetailListPreference) preference); + } + + return false; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (newValue == null) return false; + + if (preference == mTitlePref) { + mSiteSettings.setTitle(newValue.toString()); + changeEditTextPreferenceValue(mTitlePref, mSiteSettings.getTitle()); + } else if (preference == mTaglinePref) { + mSiteSettings.setTagline(newValue.toString()); + changeEditTextPreferenceValue(mTaglinePref, mSiteSettings.getTagline()); + } else if (preference == mAddressPref) { + mSiteSettings.setAddress(newValue.toString()); + changeEditTextPreferenceValue(mAddressPref, mSiteSettings.getAddress()); + } else if (preference == mLanguagePref) { + mSiteSettings.setLanguageCode(newValue.toString()); + changeLanguageValue(mSiteSettings.getLanguageCode()); + } else if (preference == mPrivacyPref) { + mSiteSettings.setPrivacy(Integer.valueOf(newValue.toString())); + setDetailListPreferenceValue(mPrivacyPref, + String.valueOf(mSiteSettings.getPrivacy()), + mSiteSettings.getPrivacyDescription()); + } else if (preference == mAllowCommentsPref || preference == mAllowCommentsNested) { + setAllowComments((Boolean) newValue); + } else if (preference == mSendPingbacksPref || preference == mSendPingbacksNested) { + setSendPingbacks((Boolean) newValue); + } else if (preference == mReceivePingbacksPref || preference == mReceivePingbacksNested) { + setReceivePingbacks((Boolean) newValue); + } else if (preference == mCloseAfterPref) { + mSiteSettings.setCloseAfter(Integer.parseInt(newValue.toString())); + if (mSiteSettings.getShouldCloseAfter()) { + mCloseAfterPref.setSummary(mSiteSettings.getCloseAfterDescription()); + } else { + mCloseAfterPref.setSummary(mSiteSettings.getCloseAfterDescription(0)); + } + } else if (preference == mSortByPref) { + mSiteSettings.setCommentSorting(Integer.parseInt(newValue.toString())); + setDetailListPreferenceValue(mSortByPref, + newValue.toString(), + mSiteSettings.getSortingDescription()); + } else if (preference == mThreadingPref) { + mSiteSettings.setThreadingLevels(Integer.parseInt(newValue.toString())); + setDetailListPreferenceValue(mThreadingPref, + newValue.toString(), + mSiteSettings.getThreadingDescription()); + } else if (preference == mPagingPref) { + mSiteSettings.setPagingCount(Integer.parseInt(newValue.toString())); + mPagingPref.setSummary(mSiteSettings.getPagingDescription()); + } else if (preference == mIdentityRequiredPreference) { + mSiteSettings.setIdentityRequired((Boolean) newValue); + } else if (preference == mUserAccountRequiredPref) { + mSiteSettings.setUserAccountRequired((Boolean) newValue); + } else if (preference == mWhitelistPref) { + updateWhitelistSettings(Integer.parseInt(newValue.toString())); + } else if (preference == mMultipleLinksPref) { + mSiteSettings.setMultipleLinks(Integer.parseInt(newValue.toString())); + mMultipleLinksPref.setSummary(getResources() + .getQuantityString(R.plurals.site_settings_multiple_links_summary, + mSiteSettings.getMultipleLinks(), + mSiteSettings.getMultipleLinks())); + } else if (preference == mUsernamePref) { + mSiteSettings.setUsername(newValue.toString()); + changeEditTextPreferenceValue(mUsernamePref, mSiteSettings.getUsername()); + } else if (preference == mPasswordPref) { + mSiteSettings.setPassword(newValue.toString()); + changeEditTextPreferenceValue(mPasswordPref, mSiteSettings.getPassword()); + } else if (preference == mLocationPref) { + mSiteSettings.setLocation((Boolean) newValue); + } else if (preference == mCategoryPref) { + mSiteSettings.setDefaultCategory(Integer.parseInt(newValue.toString())); + setDetailListPreferenceValue(mCategoryPref, + newValue.toString(), + mSiteSettings.getDefaultCategoryForDisplay()); + } else if (preference == mFormatPref) { + mSiteSettings.setDefaultFormat(newValue.toString()); + setDetailListPreferenceValue(mFormatPref, + newValue.toString(), + mSiteSettings.getDefaultPostFormatDisplay()); + } else if (preference == mImageWidthPref) { + mBlog.setMaxImageWidth(newValue.toString()); + setDetailListPreferenceValue(mImageWidthPref, + mBlog.getMaxImageWidth(), + mBlog.getMaxImageWidth()); + } else if (preference == mUploadAndLinkPref) { + mBlog.setFullSizeImage(Boolean.valueOf(newValue.toString())); + } else { + return false; + } + + mSiteSettings.saveSettings(); + + return true; + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + ListView listView = (ListView) parent; + ListAdapter listAdapter = listView.getAdapter(); + Object obj = listAdapter.getItem(position); + + if (obj != null) { + if (obj instanceof View.OnLongClickListener) { + View.OnLongClickListener longListener = (View.OnLongClickListener) obj; + return longListener.onLongClick(view); + } else if (obj instanceof PreferenceHint) { + PreferenceHint hintObj = (PreferenceHint) obj; + if (hintObj.hasHint()) { + HashMap properties = new HashMap<>(); + properties.put("hint_shown", hintObj.getHint()); + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_HINT_TOAST_SHOWN, properties); + ToastUtils.showToast(getActivity(), hintObj.getHint(), ToastUtils.Duration.SHORT); + } + return true; + } + } + + return false; + } + + @Override + public void onDismiss(DialogInterface dialog) { + mSiteSettings.saveSettings(); + mEditingList = null; + } + + @Override + public void onSettingsUpdated(Exception error) { + if (error != null) { + ToastUtils.showToast(getActivity(), R.string.error_fetch_remote_site_settings); + getActivity().finish(); + return; + } + + if (isAdded()) setPreferencesFromSiteSettings(); + } + + @Override + public void onSettingsSaved(Exception error) { + if (error != null) { + ToastUtils.showToast(WordPress.getContext(), R.string.error_post_remote_site_settings); + return; + } + mBlog.setBlogName(mSiteSettings.getTitle()); + WordPress.wpDB.saveBlog(mBlog); + EventBus.getDefault().post(new CoreEvents.BlogListChanged()); + } + + @Override + public void onCredentialsValidated(Exception error) { + if (error != null) { + ToastUtils.showToast(WordPress.getContext(), R.string.username_or_password_incorrect); + } + } + + private void setupPreferenceList(ListView prefList, Resources res) { + if (prefList == null || res == null) return; + + // customize list dividers + //noinspection deprecation + prefList.setDivider(res.getDrawable(R.drawable.preferences_divider)); + prefList.setDividerHeight(res.getDimensionPixelSize(R.dimen.site_settings_divider_height)); + // handle long clicks on preferences to display hints + prefList.setOnItemLongClickListener(this); + // required to customize (Calypso) preference views + prefList.setOnHierarchyChangeListener(this); + // remove footer divider bar + prefList.setFooterDividersEnabled(false); + //noinspection deprecation + prefList.setOverscrollFooter(res.getDrawable(R.color.transparent)); + } + + /** + * Helper method to retrieve {@link Preference} references and initialize any data. + */ + private void initPreferences() { + mTitlePref = (EditTextPreference) getChangePref(R.string.pref_key_site_title); + mTaglinePref = (EditTextPreference) getChangePref(R.string.pref_key_site_tagline); + mAddressPref = (EditTextPreference) getChangePref(R.string.pref_key_site_address); + mPrivacyPref = (DetailListPreference) getChangePref(R.string.pref_key_site_visibility); + mLanguagePref = (DetailListPreference) getChangePref(R.string.pref_key_site_language); + mUsernamePref = (EditTextPreference) getChangePref(R.string.pref_key_site_username); + mPasswordPref = (EditTextPreference) getChangePref(R.string.pref_key_site_password); + mLocationPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_location); + mCategoryPref = (DetailListPreference) getChangePref(R.string.pref_key_site_category); + mFormatPref = (DetailListPreference) getChangePref(R.string.pref_key_site_format); + mAllowCommentsPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_allow_comments); + mAllowCommentsNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_allow_comments_nested); + mSendPingbacksPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_send_pingbacks); + mSendPingbacksNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_send_pingbacks_nested); + mReceivePingbacksPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_receive_pingbacks); + mReceivePingbacksNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_receive_pingbacks_nested); + mIdentityRequiredPreference = (WPSwitchPreference) getChangePref(R.string.pref_key_site_identity_required); + mUserAccountRequiredPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_user_account_required); + mSortByPref = (DetailListPreference) getChangePref(R.string.pref_key_site_sort_by); + mThreadingPref = (DetailListPreference) getChangePref(R.string.pref_key_site_threading); + mWhitelistPref = (DetailListPreference) getChangePref(R.string.pref_key_site_whitelist); + mRelatedPostsPref = getClickPref(R.string.pref_key_site_related_posts); + mCloseAfterPref = getClickPref(R.string.pref_key_site_close_after); + mPagingPref = getClickPref(R.string.pref_key_site_paging); + mMultipleLinksPref = getClickPref(R.string.pref_key_site_multiple_links); + mModerationHoldPref = getClickPref(R.string.pref_key_site_moderation_hold); + mBlacklistPref = getClickPref(R.string.pref_key_site_blacklist); + mImageWidthPref = (DetailListPreference) getChangePref(R.string.pref_key_site_image_width); + mUploadAndLinkPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_upload_and_link_image); + mDeleteSitePref = getClickPref(R.string.pref_key_site_delete_site); + + // .com sites hide the Account category, self-hosted sites hide the Related Posts preference + if (mBlog.isDotcomFlag()) { + removeSelfHostedOnlyPreferences(); + } else { + removeDotComOnlyPreferences(); + } + + // hide all options except for Delete site and Enable Location if user is not admin + if (!mBlog.isAdmin()) hideAdminRequiredPreferences(); + } + + private void showRelatedPostsDialog() { + DialogFragment relatedPosts = new RelatedPostsDialog(); + Bundle args = new Bundle(); + args.putBoolean(RelatedPostsDialog.SHOW_RELATED_POSTS_KEY, mSiteSettings.getShowRelatedPosts()); + args.putBoolean(RelatedPostsDialog.SHOW_HEADER_KEY, mSiteSettings.getShowRelatedPostHeader()); + args.putBoolean(RelatedPostsDialog.SHOW_IMAGES_KEY, mSiteSettings.getShowRelatedPostImages()); + relatedPosts.setArguments(args); + relatedPosts.setTargetFragment(this, RELATED_POSTS_REQUEST_CODE); + relatedPosts.show(getFragmentManager(), "related-posts"); + } + + private void showNumberPickerDialog(Bundle args, int requestCode, String tag) { + NumberPickerDialog dialog = new NumberPickerDialog(); + dialog.setArguments(args); + dialog.setTargetFragment(this, requestCode); + dialog.show(getFragmentManager(), tag); + } + + private void showPagingDialog() { + Bundle args = new Bundle(); + args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, true); + args.putBoolean(NumberPickerDialog.SWITCH_ENABLED_KEY, mSiteSettings.getShouldPageComments()); + args.putString(NumberPickerDialog.SWITCH_TITLE_KEY, getString(R.string.site_settings_paging_title)); + args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_paging_title)); + args.putString(NumberPickerDialog.HEADER_TEXT_KEY, getString(R.string.site_settings_paging_dialog_header)); + args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 1); + args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.paging_limit)); + args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getPagingCount()); + showNumberPickerDialog(args, PAGING_REQUEST_CODE, "paging-dialog"); + } + + private void showCloseAfterDialog() { + Bundle args = new Bundle(); + args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, true); + args.putBoolean(NumberPickerDialog.SWITCH_ENABLED_KEY, mSiteSettings.getShouldCloseAfter()); + args.putString(NumberPickerDialog.SWITCH_TITLE_KEY, getString(R.string.site_settings_close_after_dialog_switch_text)); + args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_close_after_dialog_title)); + args.putString(NumberPickerDialog.HEADER_TEXT_KEY, getString(R.string.site_settings_close_after_dialog_header)); + args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 1); + args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.close_after_limit)); + args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getCloseAfter()); + showNumberPickerDialog(args, CLOSE_AFTER_REQUEST_CODE, "close-after-dialog"); + } + + private void showMultipleLinksDialog() { + Bundle args = new Bundle(); + args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, false); + args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_multiple_links_title)); + args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 0); + args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.max_links_limit)); + args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getMultipleLinks()); + showNumberPickerDialog(args, MULTIPLE_LINKS_REQUEST_CODE, "multiple-links-dialog"); + } + + private void setPreferencesFromSiteSettings() { + mLocationPref.setChecked(mSiteSettings.getLocation()); + changeEditTextPreferenceValue(mTitlePref, mSiteSettings.getTitle()); + changeEditTextPreferenceValue(mTaglinePref, mSiteSettings.getTagline()); + changeEditTextPreferenceValue(mAddressPref, mSiteSettings.getAddress()); + changeEditTextPreferenceValue(mUsernamePref, mSiteSettings.getUsername()); + changeEditTextPreferenceValue(mPasswordPref, mSiteSettings.getPassword()); + changeLanguageValue(mSiteSettings.getLanguageCode()); + setDetailListPreferenceValue(mPrivacyPref, + String.valueOf(mSiteSettings.getPrivacy()), + mSiteSettings.getPrivacyDescription()); + setDetailListPreferenceValue(mImageWidthPref, + mBlog.getMaxImageWidth(), + mBlog.getMaxImageWidth()); + setCategories(); + setPostFormats(); + setAllowComments(mSiteSettings.getAllowComments()); + setSendPingbacks(mSiteSettings.getSendPingbacks()); + setReceivePingbacks(mSiteSettings.getReceivePingbacks()); + setDetailListPreferenceValue(mSortByPref, + String.valueOf(mSiteSettings.getCommentSorting()), + mSiteSettings.getSortingDescription()); + setDetailListPreferenceValue(mThreadingPref, + String.valueOf(mSiteSettings.getThreadingLevels()), + mSiteSettings.getThreadingDescription()); + int approval = mSiteSettings.getManualApproval() ? + mSiteSettings.getUseCommentWhitelist() ? 0 + : -1 : 1; + setDetailListPreferenceValue(mWhitelistPref, String.valueOf(approval), getWhitelistSummary(approval)); + mMultipleLinksPref.setSummary(getResources() + .getQuantityString(R.plurals.site_settings_multiple_links_summary, + mSiteSettings.getMultipleLinks(), + mSiteSettings.getMultipleLinks())); + mUploadAndLinkPref.setChecked(mBlog.isFullSizeImage()); + mIdentityRequiredPreference.setChecked(mSiteSettings.getIdentityRequired()); + mUserAccountRequiredPref.setChecked(mSiteSettings.getUserAccountRequired()); + mThreadingPref.setValue(String.valueOf(mSiteSettings.getThreadingLevels())); + mCloseAfterPref.setSummary(mSiteSettings.getCloseAfterDescription()); + mPagingPref.setSummary(mSiteSettings.getPagingDescription()); + } + + private void setCategories() { + // Ignore if there are no changes + if (mSiteSettings.isSameCategoryList(mCategoryPref.getEntryValues())) { + mCategoryPref.setValue(String.valueOf(mSiteSettings.getDefaultCategory())); + mCategoryPref.setSummary(mSiteSettings.getDefaultCategoryForDisplay()); + return; + } + + Map categories = mSiteSettings.getCategoryNames(); + CharSequence[] entries = new CharSequence[categories.size()]; + CharSequence[] values = new CharSequence[categories.size()]; + int i = 0; + for (Integer key : categories.keySet()) { + entries[i] = categories.get(key); + values[i] = String.valueOf(key); + if (key == UNCATEGORIZED_CATEGORY_ID) { + CharSequence temp = entries[0]; + entries[0] = entries[i]; + entries[i] = temp; + temp = values[0]; + values[0] = values[i]; + values[i] = temp; + } + ++i; + } + + mCategoryPref.setEntries(entries); + mCategoryPref.setEntryValues(values); + mCategoryPref.setValue(String.valueOf(mSiteSettings.getDefaultCategory())); + mCategoryPref.setSummary(mSiteSettings.getDefaultCategoryForDisplay()); + } + + private void setPostFormats() { + // Ignore if there are no changes + if (mSiteSettings.isSameFormatList(mFormatPref.getEntryValues())) { + mFormatPref.setValue(String.valueOf(mSiteSettings.getDefaultPostFormat())); + mFormatPref.setSummary(mSiteSettings.getDefaultPostFormatDisplay()); + return; + } + + Map formats = mSiteSettings.getFormats(); + String[] formatKeys = mSiteSettings.getFormatKeys(); + String[] entries = new String[formatKeys.length]; + String[] values = new String[formatKeys.length]; + + for (int i = 0; i < entries.length; ++i) { + entries[i] = formats.get(formatKeys[i]); + values[i] = formatKeys[i]; + } + + mFormatPref.setEntries(entries); + mFormatPref.setEntryValues(values); + mFormatPref.setValue(String.valueOf(mSiteSettings.getDefaultPostFormat())); + mFormatPref.setSummary(mSiteSettings.getDefaultPostFormatDisplay()); + } + + private void setAllowComments(boolean newValue) { + mSiteSettings.setAllowComments(newValue); + mAllowCommentsPref.setChecked(newValue); + mAllowCommentsNested.setChecked(newValue); + } + + private void setSendPingbacks(boolean newValue) { + mSiteSettings.setSendPingbacks(newValue); + mSendPingbacksPref.setChecked(newValue); + mSendPingbacksNested.setChecked(newValue); + } + + private void setReceivePingbacks(boolean newValue) { + mSiteSettings.setReceivePingbacks(newValue); + mReceivePingbacksPref.setChecked(newValue); + mReceivePingbacksNested.setChecked(newValue); + } + + private void setDetailListPreferenceValue(DetailListPreference pref, String value, String summary) { + pref.setValue(value); + pref.setSummary(summary); + pref.refreshAdapter(); + } + + /** + * Helper method to perform validation and set multiple properties on an EditTextPreference. + * If newValue is equal to the current preference text no action will be taken. + */ + private void changeEditTextPreferenceValue(EditTextPreference pref, String newValue) { + if (newValue == null || pref == null || pref.getEditText().isInEditMode()) return; + + if (!newValue.equals(pref.getSummary())) { + String formattedValue = StringUtils.unescapeHTML(newValue.replaceFirst(ADDRESS_FORMAT_REGEX, "")); + + pref.setText(formattedValue); + pref.setSummary(formattedValue); + } + } + + /** + * Detail strings for the dialog are generated in the selected language. + * + * @param newValue + * languageCode + */ + private void changeLanguageValue(String newValue) { + if (mLanguagePref == null || newValue == null) return; + + if (TextUtils.isEmpty(mLanguagePref.getSummary()) || + !newValue.equals(mLanguagePref.getValue())) { + mLanguagePref.setValue(newValue); + String summary = getLanguageString(newValue, WPPrefUtils.languageLocale(newValue)); + mLanguagePref.setSummary(summary); + + // update details to display in selected locale + CharSequence[] languageCodes = mLanguagePref.getEntryValues(); + mLanguagePref.setEntries(createLanguageDisplayStrings(languageCodes)); + mLanguagePref.setDetails(createLanguageDetailDisplayStrings(languageCodes, newValue)); + mLanguagePref.refreshAdapter(); + } + } + + private String getWhitelistSummary(int value) { + if (isAdded()) { + switch (value) { + case -1: + return getString(R.string.site_settings_whitelist_none_summary); + case 0: + return getString(R.string.site_settings_whitelist_known_summary); + case 1: + return getString(R.string.site_settings_whitelist_all_summary); + } + } + return ""; + } + + private void updateWhitelistSettings(int val) { + switch (val) { + case -1: + mSiteSettings.setManualApproval(true); + mSiteSettings.setUseCommentWhitelist(false); + break; + case 0: + mSiteSettings.setManualApproval(true); + mSiteSettings.setUseCommentWhitelist(true); + break; + case 1: + mSiteSettings.setManualApproval(false); + mSiteSettings.setUseCommentWhitelist(false); + break; + } + setDetailListPreferenceValue(mWhitelistPref, + String.valueOf(val), + getWhitelistSummary(val)); + } + + private void showListEditorDialog(int titleRes, int footerRes) { + Dialog dialog = new Dialog(getActivity(), R.style.Calypso_SiteSettingsTheme); + dialog.setOnDismissListener(this); + dialog.setContentView(getListEditorView(dialog, getString(footerRes))); + dialog.show(); + WPActivityUtils.addToolbarToDialog(this, dialog, getString(titleRes)); + } + + private View getListEditorView(final Dialog dialog, String footerText) { + Context themer = new ContextThemeWrapper(getActivity(), R.style.Calypso_SiteSettingsTheme); + View view = View.inflate(themer, R.layout.list_editor, null); + ((TextView) view.findViewById(R.id.list_editor_footer_text)).setText(footerText); + + final MultiSelectListView list = (MultiSelectListView) view.findViewById(android.R.id.list); + list.setEnterMultiSelectListener(new MultiSelectListView.OnEnterMultiSelect() { + @Override + public void onEnterMultiSelect() { + WPActivityUtils.setStatusBarColor(dialog.getWindow(), R.color.action_mode_status_bar_tint); + } + }); + list.setExitMultiSelectListener(new MultiSelectListView.OnExitMultiSelect() { + @Override + public void onExitMultiSelect() { + WPActivityUtils.setStatusBarColor(dialog.getWindow(), R.color.status_bar_tint); + } + }); + list.setDeleteRequestListener(new MultiSelectListView.OnDeleteRequested() { + @Override + public boolean onDeleteRequested() { + SparseBooleanArray checkedItems = list.getCheckedItemPositions(); + + HashMap properties = new HashMap<>(); + properties.put("num_items_deleted", checkedItems.size()); + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_DELETED_LIST_ITEMS, properties); + + ListAdapter adapter = list.getAdapter(); + List itemsToRemove = new ArrayList<>(); + for (int i = 0; i < checkedItems.size(); i++) { + final int index = checkedItems.keyAt(i); + if (checkedItems.get(index)) { + itemsToRemove.add(adapter.getItem(index).toString()); + } + } + mEditingList.removeAll(itemsToRemove); + list.setAdapter(new ArrayAdapter<>(getActivity(), + R.layout.wp_simple_list_item_1, + mEditingList)); + mSiteSettings.saveSettings(); + return true; + } + }); + list.setEmptyView(view.findViewById(R.id.empty_view)); + list.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE); + list.setAdapter(new ArrayAdapter<>(getActivity(), + R.layout.wp_simple_list_item_1, + mEditingList)); + view.findViewById(R.id.fab_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + AlertDialog.Builder builder = + new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog); + final EditText input = new EditText(getActivity()); + WPPrefUtils.layoutAsInput(input); + input.setHint(R.string.site_settings_list_editor_input_hint); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String entry = input.getText().toString(); + if (!mEditingList.contains(entry)) { + mEditingList.add(entry); + list.setAdapter(new ArrayAdapter<>(getActivity(), + R.layout.wp_simple_list_item_1, + mEditingList)); + mSiteSettings.saveSettings(); + AnalyticsUtils.trackWithCurrentBlogDetails( + AnalyticsTracker.Stat.SITE_SETTINGS_ADDED_LIST_ITEM); + } + } + }); + builder.setNegativeButton(R.string.cancel, null); + AlertDialog alertDialog = builder.create(); + int spacing = getResources().getDimensionPixelSize(R.dimen.dlp_padding_start); + alertDialog.setView(input, spacing, spacing, spacing, 0); + alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + alertDialog.show(); + alertDialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); + Button positive = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE); + Button negative = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE); + if (positive != null) WPPrefUtils.layoutAsFlatButton(positive); + if (negative != null) WPPrefUtils.layoutAsFlatButton(negative); + WPActivityUtils.showKeyboard(input); + } + }); + + return view; + } + + private void removeBlog() { + if (WordPress.wpDB.deleteBlog(getActivity(), mBlog.getLocalTableBlogId())) { + StatsTable.deleteStatsForBlog(getActivity(), mBlog.getLocalTableBlogId()); // Remove stats data + AnalyticsUtils.refreshMetadata(); + ToastUtils.showToast(getActivity(), R.string.blog_removed_successfully); + WordPress.wpDB.deleteLastBlogId(); + WordPress.currentBlog = null; + getActivity().setResult(RESULT_BLOG_REMOVED); + + // If the last blog is removed and the user is not signed in wpcom, broadcast a UserSignedOut event + if (!AccountHelper.isSignedIn()) { + EventBus.getDefault().post(new CoreEvents.UserSignedOutCompletely()); + } + + // Checks for stats widgets that were synched with a blog that could be gone now. + StatsWidgetProvider.updateWidgetsOnLogout(getActivity()); + + getActivity().finish(); + } else { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); + dialogBuilder.setTitle(getResources().getText(R.string.error)); + dialogBuilder.setMessage(getResources().getText(R.string.could_not_remove_account)); + dialogBuilder.setPositiveButton(R.string.ok, null); + dialogBuilder.setCancelable(true); + dialogBuilder.create().show(); + } + } + + private void removeBlogWithConfirmation() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); + dialogBuilder.setTitle(getResources().getText(R.string.remove_account)); + dialogBuilder.setMessage(getResources().getText(R.string.sure_to_remove_account)); + dialogBuilder.setPositiveButton(getResources().getText(R.string.yes), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + removeBlog(); + } + }); + dialogBuilder.setNegativeButton(getResources().getText(R.string.no), null); + dialogBuilder.setCancelable(false); + dialogBuilder.create().show(); + } + + private boolean shouldShowListPreference(DetailListPreference preference) { + return preference != null && preference.getEntries() != null && preference.getEntries().length > 0; + } + + /** + * Generates display strings for given language codes. Used as entries in language preference. + */ + private String[] createLanguageDisplayStrings(CharSequence[] languageCodes) { + if (languageCodes == null || languageCodes.length < 1) return null; + + String[] displayStrings = new String[languageCodes.length]; + + for (int i = 0; i < languageCodes.length; ++i) { + displayStrings[i] = StringUtils.capitalize(getLanguageString( + String.valueOf(languageCodes[i]), WPPrefUtils.languageLocale(languageCodes[i].toString()))); + } + + return displayStrings; + } + + /** + * Generates detail display strings in the currently selected locale. Used as detail text + * in language preference dialog. + */ + public String[] createLanguageDetailDisplayStrings(CharSequence[] languageCodes, String locale) { + if (languageCodes == null || languageCodes.length < 1) return null; + + String[] detailStrings = new String[languageCodes.length]; + for (int i = 0; i < languageCodes.length; ++i) { + detailStrings[i] = StringUtils.capitalize( + getLanguageString(languageCodes[i].toString(), WPPrefUtils.languageLocale(locale))); + } + + return detailStrings; + } + + /** + * Return a non-null display string for a given language code. + */ + private String getLanguageString(String languageCode, Locale displayLocale) { + if (languageCode == null || languageCode.length() < 2 || languageCode.length() > 6) { + return ""; + } + + Locale languageLocale = WPPrefUtils.languageLocale(languageCode); + String displayLanguage = StringUtils.capitalize(languageLocale.getDisplayLanguage(displayLocale)); + String displayCountry = languageLocale.getDisplayCountry(displayLocale); + + if (!TextUtils.isEmpty(displayCountry)) { + return displayLanguage + " (" + displayCountry + ")"; + } + return displayLanguage; + } + + private void hideAdminRequiredPreferences() { + WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_general); + WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_account); + WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_discussion); + WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_category); + WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_format); + WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_related_posts); + } + + private void removeDotComOnlyPreferences() { + WPPrefUtils.removePreference(this, R.string.pref_key_site_general, R.string.pref_key_site_language); + WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_related_posts); + } + + private void removeSelfHostedOnlyPreferences() { + WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_account); + WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_delete_site); + } + + private Preference getChangePref(int id) { + return WPPrefUtils.getPrefAndSetChangeListener(this, id, this); + } + + private Preference getClickPref(int id) { + return WPPrefUtils.getPrefAndSetClickListener(this, id, this); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java new file mode 100644 index 000000000000..943779a41924 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java @@ -0,0 +1,824 @@ +package org.wordpress.android.ui.prefs; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.wordpress.android.R; +import org.wordpress.android.datasets.SiteSettingsTable; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.CategoryModel; +import org.wordpress.android.models.SiteSettingsModel; +import org.wordpress.android.util.WPPrefUtils; +import org.xmlrpc.android.ApiHelper; +import org.xmlrpc.android.XMLRPCCallback; +import org.xmlrpc.android.XMLRPCClientInterface; +import org.xmlrpc.android.XMLRPCFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Interface for WordPress (.com and .org) Site Settings. The {@link SiteSettingsModel} class is + * used to store the following settings: + * + * - Title + * - Tagline + * - Address + * - Privacy + * - Language + * - Username (.org only) + * - Password (.org only) + * - Location (local device setting, not saved remotely) + * - Default Category + * - Default Format + * - Related Posts + * - Allow Comments + * - Send Pingbacks + * - Receive Pingbacks + * - Identity Required + * - User Account Required + * - Close Comments After + * - Comment Sort Order + * - Comment Threading + * - Comment Paging + * - Comment User Whitelist + * - Comment Link Limit + * - Comment Moderation Hold Filter + * - Comment Blacklist Filter + * + * This class is marked abstract. This is due to the fact that .org (self-hosted) and .com sites + * expose different API's to query and edit their respective settings (even though the options + * offered by each is roughly the same). To get an instance of this interface class use the + * {@link SiteSettingsInterface#getInterface(Activity, Blog, SiteSettingsListener)} method. It will + * determine which interface ({@link SelfHostedSiteSettings} or {@link DotComSiteSettings}) is + * appropriate for the given blog. + */ + +public abstract class SiteSettingsInterface { + + /** + * Name of the {@link SharedPreferences} that is used to store local settings. + */ + public static final String SITE_SETTINGS_PREFS = "site-settings-prefs"; + + /** + * Key used to access the language preference stored in {@link SharedPreferences}. + */ + public static final String LANGUAGE_PREF_KEY = "site-settings-language-pref"; + + /** + * Key used to access the location preference stored in {@link SharedPreferences}. + */ + public static final String LOCATION_PREF_KEY = "site-settings-location-pref"; + + /** + * Key used to access the default category preference stored in {@link SharedPreferences}. + */ + public static final String DEF_CATEGORY_PREF_KEY = "site-settings-category-pref"; + + /** + * Key used to access the default post format preference stored in {@link SharedPreferences}. + */ + public static final String DEF_FORMAT_PREF_KEY = "site-settings-format-pref"; + + /** + * Identifies an Ascending (oldest to newest) sort order. + */ + public static final int ASCENDING_SORT = 0; + + /** + * Identifies an Descending (newest to oldest) sort order. + */ + public static final int DESCENDING_SORT = 1; + + /** + * Used to prefix keys in an analytics property list. + */ + protected static final String SAVED_ITEM_PREFIX = "item_saved_"; + + /** + * Key for the Standard post format. Used as default if post format is not set/known. + */ + private static final String STANDARD_POST_FORMAT_KEY = "standard"; + + /** + * Standard post format value. Used as default display value if post format is unknown. + */ + private static final String STANDARD_POST_FORMAT = "Standard"; + + /** + * Instantiates the appropriate (self-hosted or .com) SiteSettingsInterface. + */ + public static SiteSettingsInterface getInterface(Activity host, Blog blog, SiteSettingsListener listener) { + if (host == null || blog == null) return null; + + if (blog.isDotcomFlag()) { + return new DotComSiteSettings(host, blog, listener); + } else { + return new SelfHostedSiteSettings(host, blog, listener); + } + } + + /** + * Returns an instance of the {@link this#SITE_SETTINGS_PREFS} {@link SharedPreferences}. + */ + public static SharedPreferences siteSettingsPreferences(Context context) { + return context.getSharedPreferences(SITE_SETTINGS_PREFS, Context.MODE_PRIVATE); + } + + /** + * Gets the geo-tagging value stored in {@link SharedPreferences}, false by default. + */ + public static boolean getGeotagging(Context context) { + return siteSettingsPreferences(context).getBoolean(LOCATION_PREF_KEY, false); + } + + /** + * Gets the default category value stored in {@link SharedPreferences}, 0 by default. + */ + public static String getDefaultCategory(Context context) { + int id = siteSettingsPreferences(context).getInt(DEF_CATEGORY_PREF_KEY, 0); + + if (id != 0) { + CategoryModel category = new CategoryModel(); + Cursor cursor = SiteSettingsTable.getCategory(id); + if (cursor != null && cursor.moveToFirst()) { + category.deserializeFromDatabase(cursor); + return category.name; + } + } + + return ""; + } + + /** + * Gets the default post format value stored in {@link SharedPreferences}, "" by default. + */ + public static String getDefaultFormat(Context context) { + return siteSettingsPreferences(context).getString(DEF_FORMAT_PREF_KEY, ""); + } + + /** + * Thrown when provided credentials are not valid. + */ + public class AuthenticationError extends Exception { } + + /** + * Interface callbacks for settings events. + */ + public interface SiteSettingsListener { + /** + * Called when settings have been updated with remote changes. + * + * @param error + * null if successful + */ + void onSettingsUpdated(Exception error); + + /** + * Called when attempt to update remote settings is finished. + * + * @param error + * null if successful + */ + void onSettingsSaved(Exception error); + + /** + * Called when a request to validate current credentials has completed. + * + * @param error + * null if successful + */ + void onCredentialsValidated(Exception error); + } + + /** + * {@link SiteSettingsInterface} implementations should use this method to start a background + * task to load settings data from a remote source. + */ + protected abstract void fetchRemoteData(); + + protected final Activity mActivity; + protected final Blog mBlog; + protected final SiteSettingsListener mListener; + protected final SiteSettingsModel mSettings; + protected final SiteSettingsModel mRemoteSettings; + + private final Map mLanguageCodes; + + protected SiteSettingsInterface(Activity host, Blog blog, SiteSettingsListener listener) { + mActivity = host; + mBlog = blog; + mListener = listener; + mSettings = new SiteSettingsModel(); + mRemoteSettings = new SiteSettingsModel(); + mLanguageCodes = WPPrefUtils.generateLanguageMap(host); + } + + public void saveSettings() { + SiteSettingsTable.saveSettings(mSettings); + siteSettingsPreferences(mActivity).edit().putString(LANGUAGE_PREF_KEY, mSettings.language).apply(); + siteSettingsPreferences(mActivity).edit().putBoolean(LOCATION_PREF_KEY, mSettings.location).apply(); + siteSettingsPreferences(mActivity).edit().putInt(DEF_CATEGORY_PREF_KEY, mSettings.defaultCategory).apply(); + siteSettingsPreferences(mActivity).edit().putString(DEF_FORMAT_PREF_KEY, mSettings.defaultPostFormat).apply(); + } + + public @NonNull String getTitle() { + return mSettings.title == null ? "" : mSettings.title; + } + + public @NonNull String getTagline() { + return mSettings.tagline == null ? "" : mSettings.tagline; + } + + public @NonNull String getAddress() { + return mSettings.address == null ? "" : mSettings.address; + } + + public int getPrivacy() { + return mSettings.privacy; + } + + public @NonNull String getPrivacyDescription() { + if (mActivity != null) { + switch (getPrivacy()) { + case -1: + return mActivity.getString(R.string.site_settings_privacy_private_summary); + case 0: + return mActivity.getString(R.string.site_settings_privacy_hidden_summary); + case 1: + return mActivity.getString(R.string.site_settings_privacy_public_summary); + } + } + return ""; + } + + public @NonNull String getLanguageCode() { + return mSettings.language == null ? "" : mSettings.language; + } + + public @NonNull String getUsername() { + return mSettings.username == null ? "" : mSettings.username; + } + + public @NonNull String getPassword() { + return mSettings.password == null ? "" : mSettings.password; + } + + public boolean getLocation() { + return mSettings.location; + } + + public @NonNull Map getFormats() { + if (mSettings.postFormats == null) mSettings.postFormats = new HashMap<>(); + return mSettings.postFormats; + } + + public @NonNull String[] getFormatKeys() { + if (mSettings.postFormatKeys == null) mSettings.postFormatKeys = new String[0]; + return mSettings.postFormatKeys; + } + + public @NonNull CategoryModel[] getCategories() { + if (mSettings.categories == null) mSettings.categories = new CategoryModel[0]; + return mSettings.categories; + } + + public @NonNull Map getCategoryNames() { + Map categoryNames = new HashMap<>(); + if (mSettings.categories != null && mSettings.categories.length > 0) { + for (CategoryModel model : mSettings.categories) { + categoryNames.put(model.id, model.name); + } + } + + return categoryNames; + } + + public int getDefaultCategory() { + return mSettings.defaultCategory; + } + + public @NonNull String getDefaultCategoryForDisplay() { + for (CategoryModel model : getCategories()) { + if (model != null && model.id == getDefaultCategory()) { + return model.name; + } + } + + return ""; + } + + public @NonNull String getDefaultPostFormat() { + if (TextUtils.isEmpty(mSettings.defaultPostFormat) || !getFormats().containsKey(mSettings.defaultPostFormat)) { + mSettings.defaultPostFormat = STANDARD_POST_FORMAT_KEY; + } + return mSettings.defaultPostFormat; + } + + public @NonNull String getDefaultPostFormatDisplay() { + String defaultFormat = getFormats().get(getDefaultPostFormat()); + if (TextUtils.isEmpty(defaultFormat)) defaultFormat = STANDARD_POST_FORMAT; + return defaultFormat; + } + + public boolean getShowRelatedPosts() { + return mSettings.showRelatedPosts; + } + + public boolean getShowRelatedPostHeader() { + return mSettings.showRelatedPostHeader; + } + + public boolean getShowRelatedPostImages() { + return mSettings.showRelatedPostImages; + } + + public boolean getAllowComments() { + return mSettings.allowComments; + } + + public boolean getSendPingbacks() { + return mSettings.sendPingbacks; + } + + public boolean getReceivePingbacks() { + return mSettings.receivePingbacks; + } + + public boolean getShouldCloseAfter() { + return mSettings.shouldCloseAfter; + } + + public int getCloseAfter() { + return mSettings.closeCommentAfter; + } + + public @NonNull String getCloseAfterDescription() { + return getCloseAfterDescription(getCloseAfter()); + } + + public @NonNull String getCloseAfterDescription(int period) { + if (mActivity == null) return ""; + + if (period == 0) return mActivity.getString(R.string.never); + return mActivity.getResources().getQuantityString(R.plurals.days_quantity, period, period); + } + + public int getCommentSorting() { + return mSettings.sortCommentsBy; + } + + public @NonNull String getSortingDescription() { + if (mActivity == null) return ""; + + int order = getCommentSorting(); + switch (order) { + case SiteSettingsInterface.ASCENDING_SORT: + return mActivity.getString(R.string.oldest_first); + case SiteSettingsInterface.DESCENDING_SORT: + return mActivity.getString(R.string.newest_first); + default: + return mActivity.getString(R.string.unknown); + } + } + + public boolean getShouldThreadComments() { + return mSettings.shouldThreadComments; + } + + public int getThreadingLevels() { + return mSettings.threadingLevels; + } + + public @NonNull String getThreadingDescription() { + if (mActivity == null) return ""; + + int levels = getThreadingLevels(); + if (levels <= 1) return mActivity.getString(R.string.none); + return String.format(mActivity.getString(R.string.site_settings_threading_summary), levels); + } + + public boolean getShouldPageComments() { + return mSettings.shouldPageComments; + } + + public int getPagingCount() { + return mSettings.commentsPerPage; + } + + public @NonNull String getPagingDescription() { + if (mActivity == null) return ""; + + int count = getPagingCount(); + if (count == 0) return mActivity.getString(R.string.none); + return mActivity.getResources().getQuantityString(R.plurals.site_settings_paging_summary, count, count); + } + + public boolean getManualApproval() { + return mSettings.commentApprovalRequired; + } + + public boolean getIdentityRequired() { + return mSettings.commentsRequireIdentity; + } + + public boolean getUserAccountRequired() { + return mSettings.commentsRequireUserAccount; + } + + public boolean getUseCommentWhitelist() { + return mSettings.commentAutoApprovalKnownUsers; + } + + public int getMultipleLinks() { + return mSettings.maxLinks; + } + + public @NonNull List getModerationKeys() { + if (mSettings.holdForModeration == null) return new ArrayList<>(); + return mSettings.holdForModeration; + } + + public @NonNull List getBlacklistKeys() { + if (mSettings.blacklist == null) return new ArrayList<>(); + return mSettings.blacklist; + } + + public void setTitle(String title) { + mSettings.title = title; + } + + public void setTagline(String tagline) { + mSettings.tagline = tagline; + } + + public void setAddress(String address) { + mSettings.address = address; + } + + public void setPrivacy(int privacy) { + mSettings.privacy = privacy; + } + + public void setLanguageCode(String languageCode) { + mSettings.language = languageCode; + mSettings.languageId = Integer.valueOf(mLanguageCodes.get(languageCode)); + } + + public void setLanguageId(int languageId) { + // want to prevent O(n) language code lookup if there is no change + if (mSettings.languageId != languageId) { + mSettings.languageId = languageId; + mSettings.language = languageIdToLanguageCode(Integer.toString(languageId)); + } + } + + public void setUsername(String username) { + mSettings.username = username; + } + + public void setPassword(String password) { + mSettings.password = password; + } + + public void setLocation(boolean location) { + mSettings.location = location; + } + + public void setAllowComments(boolean allowComments) { + mSettings.allowComments = allowComments; + } + + public void setSendPingbacks(boolean sendPingbacks) { + mSettings.sendPingbacks = sendPingbacks; + } + + public void setReceivePingbacks(boolean receivePingbacks) { + mSettings.receivePingbacks = receivePingbacks; + } + + public void setShouldCloseAfter(boolean shouldCloseAfter) { + mSettings.shouldCloseAfter = shouldCloseAfter; + } + + public void setCloseAfter(int period) { + mSettings.closeCommentAfter = period; + } + + public void setCommentSorting(int method) { + mSettings.sortCommentsBy = method; + } + + public void setShouldThreadComments(boolean shouldThread) { + mSettings.shouldThreadComments = shouldThread; + } + + public void setThreadingLevels(int levels) { + mSettings.threadingLevels = levels; + } + + public void setShouldPageComments(boolean shouldPage) { + mSettings.shouldPageComments= shouldPage; + } + + public void setPagingCount(int count) { + mSettings.commentsPerPage = count; + } + + public void setManualApproval(boolean required) { + mSettings.commentApprovalRequired = required; + } + + public void setIdentityRequired(boolean required) { + mSettings.commentsRequireIdentity = required; + } + + public void setUserAccountRequired(boolean required) { + mSettings.commentsRequireUserAccount = required; + } + + public void setUseCommentWhitelist(boolean useWhitelist) { + mSettings.commentAutoApprovalKnownUsers = useWhitelist; + } + + public void setMultipleLinks(int count) { + mSettings.maxLinks = count; + } + + public void setModerationKeys(List keys) { + mSettings.holdForModeration = keys; + } + + public void setBlacklistKeys(List keys) { + mSettings.blacklist = keys; + } + + public void setDefaultCategory(int category) { + mSettings.defaultCategory = category; + } + + /** + * Sets the default post format. + * + * @param format + * if null or empty default format is set to {@link SiteSettingsInterface#STANDARD_POST_FORMAT_KEY} + */ + public void setDefaultFormat(String format) { + if (TextUtils.isEmpty(format)) { + mSettings.defaultPostFormat = STANDARD_POST_FORMAT_KEY; + } else { + mSettings.defaultPostFormat = format.toLowerCase(); + } + } + + public void setShowRelatedPosts(boolean relatedPosts) { + mSettings.showRelatedPosts = relatedPosts; + } + + public void setShowRelatedPostHeader(boolean showHeader) { + mSettings.showRelatedPostHeader = showHeader; + } + + public void setShowRelatedPostImages(boolean showImages) { + mSettings.showRelatedPostImages = showImages; + } + + /** + * Determines if the current Moderation Hold list contains a given value. + */ + public boolean moderationHoldListContains(String value) { + return getModerationKeys().contains(value); + } + + /** + * Determines if the current Blacklist list contains a given value. + */ + public boolean blacklistListContains(String value) { + return getBlacklistKeys().contains(value); + } + + /** + * Checks if the provided list of post format IDs is the same (order dependent) as the current + * list of Post Formats in the local settings object. + * + * @param ids + * an array of post format IDs + * @return + * true unless the provided IDs are different from the current IDs or in a different order + */ + public boolean isSameFormatList(CharSequence[] ids) { + if (ids == null) return mSettings.postFormats == null; + if (mSettings.postFormats == null || ids.length != mSettings.postFormats.size()) return false; + + String[] keys = mSettings.postFormats.keySet().toArray(new String[mSettings.postFormats.size()]); + for (int i = 0; i < ids.length; ++i) { + if (!keys[i].equals(ids[i])) return false; + } + + return true; + } + + /** + * Checks if the provided list of category IDs is the same (order dependent) as the current + * list of Categories in the local settings object. + * + * @param ids + * an array of integers stored as Strings (for convenience) + * @return + * true unless the provided IDs are different from the current IDs or in a different order + */ + public boolean isSameCategoryList(CharSequence[] ids) { + if (ids == null) return mSettings.categories == null; + if (mSettings.categories == null || ids.length != mSettings.categories.length) return false; + + for (int i = 0; i < ids.length; ++i) { + if (Integer.valueOf(ids[i].toString()) != mSettings.categories[i].id) return false; + } + + return true; + } + + /** + * Needed so that subclasses can be created before initializing. The final member variables + * are null until object has been created so XML-RPC callbacks will not run. + * + * @return + * returns itself for the convenience of + * {@link SiteSettingsInterface#getInterface(Activity, Blog, SiteSettingsListener)} + */ + public SiteSettingsInterface init(boolean fetchRemote) { + loadCachedSettings(); + + if (fetchRemote) { + fetchRemoteData(); + fetchPostFormats(); + } + + return this; + } + + /** + * If there is a change in verification status the listener is notified. + */ + protected void credentialsVerified(boolean valid) { + Exception e = valid ? null : new AuthenticationError(); + if (mSettings.hasVerifiedCredentials != valid) notifyCredentialsVerifiedOnUiThread(e); + mRemoteSettings.hasVerifiedCredentials = mSettings.hasVerifiedCredentials = valid; + } + + /** + * Helper method to create an XML-RPC interface for the current blog. + */ + protected XMLRPCClientInterface instantiateInterface() { + if (mBlog == null) return null; + return XMLRPCFactory.instantiate(mBlog.getUri(), mBlog.getHttpuser(), mBlog.getHttppassword()); + } + + /** + * Language IDs, used only by WordPress, are integer values that map to a language code. + * https://github.com/Automattic/calypso-pre-oss/blob/72c2029b0805a73b749a2b64dd1d8655cae528d0/config/production.json#L86-L227 + * + * Language codes are unique two-letter identifiers defined by ISO 639-1. Region dialects can + * be defined by appending a -** where ** is the region code (en-GB -> English, Great Britain). + * https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes + */ + protected String languageIdToLanguageCode(String id) { + if (id != null) { + for (String key : mLanguageCodes.keySet()) { + if (id.equals(mLanguageCodes.get(key))) { + return key; + } + } + } + + return ""; + } + + /** + * Need to defer loading the cached settings to a thread so it completes after initialization. + */ + private void loadCachedSettings() { + Cursor localSettings = SiteSettingsTable.getSettings(mBlog.getRemoteBlogId()); + + if (localSettings != null) { + Map cachedModels = SiteSettingsTable.getAllCategories(); + mSettings.deserializeOptionsDatabaseCursor(localSettings, cachedModels); + mSettings.language = languageIdToLanguageCode(Integer.toString(mSettings.languageId)); + if (mSettings.language == null) { + setLanguageCode(Locale.getDefault().getLanguage()); + } + mRemoteSettings.language = mSettings.language; + mRemoteSettings.languageId = mSettings.languageId; + mRemoteSettings.location = mSettings.location; + localSettings.close(); + notifyUpdatedOnUiThread(null); + } else { + mSettings.isInLocalTable = false; + setAddress(mBlog.getHomeURL()); + setUsername(mBlog.getUsername()); + setPassword(mBlog.getPassword()); + setTitle(mBlog.getBlogName()); + } + } + + /** + * Gets available post formats via XML-RPC. Since both self-hosted and .com sites retrieve the + * format list via XML-RPC there is no need to implement this in the sub-classes. + */ + private void fetchPostFormats() { + XMLRPCClientInterface client = instantiateInterface(); + if (client == null) return; + + Map args = new HashMap<>(); + args.put(ApiHelper.Params.SHOW_SUPPORTED_POST_FORMATS, "true"); + Object[] params = { mBlog.getRemoteBlogId(), mBlog.getUsername(), + mBlog.getPassword(), args}; + client.callAsync(new XMLRPCCallback() { + @Override + public void onSuccess(long id, Object result) { + credentialsVerified(true); + + if (result != null && result instanceof HashMap) { + Map resultMap = (HashMap) result; + Map allFormats; + Object[] supportedFormats; + if (resultMap.containsKey("supported")) { + allFormats = (Map) resultMap.get("all"); + supportedFormats = (Object[]) resultMap.get("supported"); + } else { + allFormats = resultMap; + supportedFormats = allFormats.keySet().toArray(); + } + + mRemoteSettings.postFormats = new HashMap<>(); + mRemoteSettings.postFormats.put("standard", "Standard"); + for (Object supportedFormat : supportedFormats) { + if (allFormats.containsKey(supportedFormat)) { + mRemoteSettings.postFormats.put(supportedFormat.toString(), allFormats.get(supportedFormat).toString()); + } + } + mSettings.postFormats = new HashMap<>(mRemoteSettings.postFormats); + String[] formatKeys = new String[mRemoteSettings.postFormats.size()]; + mRemoteSettings.postFormatKeys = mRemoteSettings.postFormats.keySet().toArray(formatKeys); + mSettings.postFormatKeys = mRemoteSettings.postFormatKeys.clone(); + + notifyUpdatedOnUiThread(null); + } + } + + @Override + public void onFailure(long id, Exception error) { + } + }, ApiHelper.Methods.GET_POST_FORMATS, params); + } + + /** + * Notifies listener that credentials have been validated or are incorrect. + */ + private void notifyCredentialsVerifiedOnUiThread(final Exception error) { + if (mActivity == null || mListener == null) return; + + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mListener.onCredentialsValidated(error); + } + }); + } + + /** + * Notifies listener that settings have been updated with the latest remote data. + */ + protected void notifyUpdatedOnUiThread(final Exception error) { + if (mActivity == null || mActivity.isFinishing() || mListener == null) return; + + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mListener.onSettingsUpdated(error); + } + }); + } + + /** + * Notifies listener that settings have been saved or an error occurred while saving. + */ + protected void notifySavedOnUiThread(final Exception error) { + if (mActivity == null || mListener == null) return; + + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mListener.onSettingsSaved(error); + } + }); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java new file mode 100644 index 000000000000..2ab7e866e4dc --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java @@ -0,0 +1,188 @@ +package org.wordpress.android.ui.prefs; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.support.annotation.NonNull; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.util.WPActivityUtils; +import org.wordpress.android.util.WPPrefUtils; + +/** + * Standard EditTextPreference that has attributes to limit summary length. + * + * Created for and used by {@link SiteSettingsFragment} to style some Preferences. + * + * When declaring this class in a layout file you can use the following attributes: + * - app:summaryLines : sets the number of lines to display in the Summary field + * (see {@link TextView#setLines(int)} for details) + * - app:maxSummaryLines : sets the maximum number of lines the Summary field can display + * (see {@link TextView#setMaxLines(int)} for details) + * - app:longClickHint : sets the string to be shown in a Toast when preference is long clicked + */ + +public class SummaryEditTextPreference extends EditTextPreference implements PreferenceHint { + private int mLines; + private int mMaxLines; + private String mHint; + private AlertDialog mDialog; + private EditText mEditText; + private int mWhichButtonClicked; + + public SummaryEditTextPreference(Context context) { + super(context); + } + + public SummaryEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SummaryEditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + mLines = -1; + mMaxLines = -1; + + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SummaryEditTextPreference); + + for (int i = 0; i < array.getIndexCount(); ++i) { + int index = array.getIndex(i); + if (index == R.styleable.SummaryEditTextPreference_summaryLines) { + mLines = array.getInt(index, -1); + } else if (index == R.styleable.SummaryEditTextPreference_maxSummaryLines) { + mMaxLines = array.getInt(index, -1); + } else if (index == R.styleable.SummaryEditTextPreference_longClickHint) { + mHint = array.getString(index); + } + } + + array.recycle(); + } + + @Override + protected void onBindView(@NonNull View view) { + super.onBindView(view); + + TextView titleView = (TextView) view.findViewById(android.R.id.title); + TextView summaryView = (TextView) view.findViewById(android.R.id.summary); + + if (titleView != null) WPPrefUtils.layoutAsSubhead(titleView); + + if (summaryView != null) { + WPPrefUtils.layoutAsBody1(summaryView); + summaryView.setEllipsize(TextUtils.TruncateAt.END); + summaryView.setInputType(getEditText().getInputType()); + if (mLines != -1) summaryView.setLines(mLines); + if (mMaxLines != -1) summaryView.setMaxLines(mMaxLines); + } + } + + @Override + protected void showDialog(Bundle state) { + Context context = getContext(); + Resources res = context.getResources(); + AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Calypso_AlertDialog); + View titleView = View.inflate(getContext(), R.layout.detail_list_preference_title, null); + mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE; + + builder.setPositiveButton(R.string.ok, this); + builder.setNegativeButton(res.getString(R.string.cancel).toUpperCase(), this); + if (titleView != null) { + TextView titleText = (TextView) titleView.findViewById(R.id.title); + if (titleText != null) { + titleText.setText(getTitle()); + } + + builder.setCustomTitle(titleView); + } else { + builder.setTitle(getTitle()); + } + + View view = View.inflate(getContext(), getDialogLayoutResource(), null); + if (view != null) { + onBindDialogView(view); + builder.setView(view); + } + + if ((mDialog = builder.create()) == null) return; + + if (state != null) { + mDialog.onRestoreInstanceState(state); + } + mDialog.setOnDismissListener(this); + mDialog.show(); + + Button positive = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); + Button negative = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE); + if (positive != null) WPPrefUtils.layoutAsFlatButton(positive); + if (negative != null) WPPrefUtils.layoutAsFlatButton(negative); + } + + @Override + protected void onBindDialogView(final View view) { + super.onBindDialogView(view); + + if (view != null) { + mEditText = getEditText(); + ViewParent oldParent = mEditText.getParent(); + if (oldParent != view) { + if (oldParent != null) { + ((ViewGroup) oldParent).removeView(mEditText); + } + ((View) oldParent).setPadding(((View) oldParent).getPaddingLeft(), 0, ((View) oldParent).getPaddingRight(), ((View) oldParent).getPaddingBottom()); + onAddEditTextToDialogView(view, mEditText); + } + WPPrefUtils.layoutAsInput(mEditText); + mEditText.setSelection(mEditText.getText().length()); + WPActivityUtils.showKeyboard(mEditText); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + WPActivityUtils.hideKeyboard(mEditText); + mWhichButtonClicked = which; + } + + @Override + public void onDismiss(DialogInterface dialog) { + onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE); + mDialog = null; + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + if (positiveResult) { + callChangeListener(getEditText().getText()); + } + } + + @Override + public boolean hasHint() { + return !TextUtils.isEmpty(mHint); + } + + @Override + public String getHint() { + return mHint; + } + + @Override + public void setHint(String hint) { + mHint = hint; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java new file mode 100644 index 000000000000..befad0ffd87b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java @@ -0,0 +1,73 @@ +package org.wordpress.android.ui.prefs; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.preference.Preference; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.widgets.TypefaceCache; + +public class WPPreference extends Preference implements PreferenceHint { + private String mHint; + + public WPPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DetailListPreference); + + for (int i = 0; i < array.getIndexCount(); ++i) { + int index = array.getIndex(i); + if (index == R.styleable.DetailListPreference_longClickHint) { + mHint = array.getString(index); + } + } + + array.recycle(); + } + + @Override + protected void onBindView(@NonNull View view) { + super.onBindView(view); + + Resources res = getContext().getResources(); + Typeface typeface = TypefaceCache.getTypeface(getContext(), + TypefaceCache.FAMILY_OPEN_SANS, + Typeface.NORMAL, + TypefaceCache.VARIATION_NORMAL); + TextView titleView = (TextView) view.findViewById(android.R.id.title); + TextView summaryView = (TextView) view.findViewById(android.R.id.summary); + if (titleView != null) { + titleView.setTypeface(typeface); + titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_large)); + titleView.setTextColor(res.getColor(isEnabled() ? R.color.grey_dark : R.color.grey_lighten_10)); + } + if (summaryView != null) { + summaryView.setTypeface(typeface); + summaryView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_medium)); + summaryView.setTextColor(res.getColor(isEnabled() ? R.color.grey_darken_10 : R.color.grey_lighten_10)); + } + } + + @Override + public boolean hasHint() { + return !TextUtils.isEmpty(mHint); + } + + @Override + public String getHint() { + return mHint; + } + + @Override + public void setHint(String hint) { + mHint = hint; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java new file mode 100644 index 000000000000..7f39c0e009d3 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java @@ -0,0 +1,68 @@ +package org.wordpress.android.ui.prefs; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.preference.SwitchPreference; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.widgets.TypefaceCache; + +public class WPSwitchPreference extends SwitchPreference implements PreferenceHint { + private String mHint; + + public WPSwitchPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SummaryEditTextPreference); + + for (int i = 0; i < array.getIndexCount(); ++i) { + int index = array.getIndex(i); + if (index == R.styleable.SummaryEditTextPreference_longClickHint) { + mHint = array.getString(index); + } + } + + array.recycle(); + } + + @Override + protected void onBindView(@NonNull View view) { + super.onBindView(view); + + TextView titleView = (TextView) view.findViewById(android.R.id.title); + if (titleView != null) { + Resources res = getContext().getResources(); + Typeface typeface = TypefaceCache.getTypeface(getContext(), + TypefaceCache.FAMILY_OPEN_SANS, + Typeface.NORMAL, + TypefaceCache.VARIATION_NORMAL); + + titleView.setTypeface(typeface); + titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_large)); + titleView.setTextColor(res.getColor(isEnabled() ? R.color.grey_dark : R.color.grey_lighten_10)); + } + } + + @Override + public boolean hasHint() { + return !TextUtils.isEmpty(mHint); + } + + @Override + public String getHint() { + return mHint; + } + + @Override + public void setHint(String hint) { + mHint = hint; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java index 4a716a398df8..a0b47e7ac38a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java @@ -5,7 +5,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceCategory; @@ -16,17 +15,10 @@ import android.support.annotation.NonNull; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; -import android.support.v7.widget.Toolbar; import android.text.TextUtils; -import android.util.TypedValue; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.ListView; import com.android.volley.VolleyError; import com.wordpress.rest.RestRequest; @@ -47,6 +39,7 @@ import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.MapUtils; import org.wordpress.android.util.UrlUtils; +import org.wordpress.android.util.WPActivityUtils; import java.util.ArrayList; import java.util.List; @@ -443,11 +436,11 @@ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, @NonNull super.onPreferenceTreeClick(preferenceScreen, preference); if (preference instanceof PreferenceScreen) { - addToolbarToPreferenceScreen((PreferenceScreen) preference); - } - - // Bump Analytics - if (preference instanceof PreferenceScreen) { + Dialog prefDialog = ((PreferenceScreen) preference).getDialog(); + if (prefDialog != null) { + String title = String.valueOf(preference.getTitle()); + WPActivityUtils.addToolbarToDialog(this, prefDialog, title); + } AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_STREAMS_OPENED); } else { AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_DETAILS_OPENED); @@ -455,58 +448,4 @@ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, @NonNull return false; } - - // Hack! PreferenceScreens don't show the toolbar, so we'll manually add one - // See: http://stackoverflow.com/a/27455363/309558 - private void addToolbarToPreferenceScreen(PreferenceScreen preferenceScreen) { - final Dialog dialog = preferenceScreen.getDialog(); - if (!isAdded() || dialog == null) { - return; - } - - Toolbar toolbar; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - if (dialog.findViewById(android.R.id.list) == null) { - return; - } - - LinearLayout root = (LinearLayout) dialog.findViewById(android.R.id.list).getParent(); - toolbar = (Toolbar) LayoutInflater.from(getActivity()).inflate(R.layout.toolbar, root, false); - root.addView(toolbar, 0); - } else { - if (dialog.findViewById(android.R.id.content) == null) { - return; - } - - ViewGroup root = (ViewGroup) dialog.findViewById(android.R.id.content); - if (!(root.getChildAt(0) instanceof ListView)) { - return; - } - - ListView content = (ListView) root.getChildAt(0); - root.removeAllViews(); - - toolbar = (Toolbar) LayoutInflater.from(getActivity()).inflate(R.layout.toolbar, root, false); - int height; - TypedValue tv = new TypedValue(); - if (getActivity().getTheme().resolveAttribute(android.support.design.R.attr.actionBarSize, tv, true)) { - height = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics()); - } else{ - height = toolbar.getHeight(); - } - - content.setPadding(0, height, 0, 0); - root.addView(content); - root.addView(toolbar); - } - - toolbar.setTitle(preferenceScreen.getTitle()); - toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp); - toolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dialog.dismiss(); - } - }); - } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPActivityUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPActivityUtils.java index a239596b7b26..ff7e1c4d2145 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/WPActivityUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/WPActivityUtils.java @@ -1,21 +1,122 @@ package org.wordpress.android.util; import android.app.Activity; +import android.app.Dialog; +import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; +import android.os.Build; +import android.os.Handler; import android.preference.PreferenceManager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; import android.text.TextUtils; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; +import org.wordpress.android.R; import org.wordpress.android.ui.prefs.SettingsFragment; import java.util.Locale; public class WPActivityUtils { + private static final long SHOW_KEYBOARD_DELAY = 250; + + // Hack! PreferenceScreens don't show the toolbar, so we'll manually add one + // See: http://stackoverflow.com/a/27455363/309558 + public static void addToolbarToDialog(final Fragment context, final Dialog dialog, String title) { + if (!context.isAdded() || dialog == null) { + return; + } + + Toolbar toolbar; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + if (dialog.findViewById(android.R.id.list) == null) { + return; + } + + LinearLayout root = (LinearLayout) dialog.findViewById(android.R.id.list).getParent(); + toolbar = (Toolbar) LayoutInflater.from(context.getActivity()).inflate(org.wordpress.android.R.layout.toolbar, root, false); + root.addView(toolbar, 0); + } else { + if (dialog.findViewById(android.R.id.content) == null) { + return; + } + + ViewGroup root = (ViewGroup) dialog.findViewById(android.R.id.content); + if (!(root.getChildAt(0) instanceof ListView)) { + return; + } + + ListView content = (ListView) root.getChildAt(0); + root.removeAllViews(); + + toolbar = (Toolbar) LayoutInflater.from(context.getActivity()).inflate(org.wordpress.android.R.layout.toolbar, root, false); + int height; + TypedValue tv = new TypedValue(); + if (context.getActivity().getTheme().resolveAttribute(org.wordpress.android.R.attr.actionBarSize, tv, true)) { + height = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics()); + } else{ + height = toolbar.getHeight(); + } + + content.setPadding(0, height, 0, 0); + root.addView(content); + root.addView(toolbar); + } + + dialog.getWindow().setWindowAnimations(R.style.DialogAnimations); + + TextView titleView = (TextView) toolbar.findViewById(R.id.toolbar_title); + titleView.setVisibility(View.VISIBLE); + titleView.setText(title); + + toolbar.setTitle(""); + toolbar.setContentInsetsAbsolute(0, 0); + toolbar.setNavigationIcon(org.wordpress.android.R.drawable.ic_arrow_back_white_24dp); + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }); + } + + public static void setStatusBarColor(Window window, int color) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + //noinspection deprecation + window.setStatusBarColor(window.getContext().getResources().getColor(color)); + } + } + + public static void showKeyboard(final View view) { + (new Handler()).postDelayed(new Runnable() { + public void run() { + InputMethodManager inputMethodManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.toggleSoftInputFromWindow(view.getWindowToken(), InputMethodManager.SHOW_IMPLICIT, 0); + } + }, SHOW_KEYBOARD_DELAY); + } + + public static void hideKeyboard(final View view) { + InputMethodManager inputMethodManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + public static void applyLocale(Activity context, boolean restart) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPPrefUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPPrefUtils.java new file mode 100644 index 000000000000..618dcaca1e83 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/WPPrefUtils.java @@ -0,0 +1,226 @@ +package org.wordpress.android.util; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Typeface; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.text.TextUtils; +import android.util.TypedValue; +import android.widget.EditText; +import android.widget.TextView; + +import org.wordpress.android.widgets.TypefaceCache; + +import org.wordpress.android.R; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Design guidelines for Calypso-styled Site Settings (and likely other screens) + */ + +public class WPPrefUtils { + + /** + * Length of a {@link String} (representing a language code) when there is no region included. + * For example: "en" contains no region, "en_US" contains a region (US) + * + * Used to parse a language code {@link String} when creating a {@link Locale}. + */ + private static final int NO_REGION_LANG_CODE_LEN = 2; + + /** + * Index of a language code {@link String} where the region code begins. The language code + * format is cc_rr, where cc is the country code (e.g. en, es, az) and rr is the region code + * (e.g. us, au, gb). + */ + private static final int REGION_SUBSTRING_INDEX = 3; + + /** + * Gets a preference and sets the {@link android.preference.Preference.OnPreferenceChangeListener}. + */ + public static Preference getPrefAndSetClickListener(PreferenceFragment prefFrag, + int id, + Preference.OnPreferenceClickListener listener) { + Preference pref = prefFrag.findPreference(prefFrag.getString(id)); + if (pref != null) pref.setOnPreferenceClickListener(listener); + return pref; + } + + /** + * Gets a preference and sets the {@link android.preference.Preference.OnPreferenceChangeListener}. + */ + public static Preference getPrefAndSetChangeListener(PreferenceFragment prefFrag, + int id, + Preference.OnPreferenceChangeListener listener) { + Preference pref = prefFrag.findPreference(prefFrag.getString(id)); + if (pref != null) pref.setOnPreferenceChangeListener(listener); + return pref; + } + + /** + * Removes a {@link Preference} from the {@link PreferenceCategory} with the given key. + */ + public static void removePreference(PreferenceFragment prefFrag, int parentKey, int prefKey) { + String parentName = prefFrag.getString(parentKey); + String prefName = prefFrag.getString(prefKey); + PreferenceGroup parent = (PreferenceGroup) prefFrag.findPreference(parentName); + Preference child = prefFrag.findPreference(prefName); + + if (parent != null && child != null) { + parent.removePreference(child); + } + } + + /** + * Font : Open Sans + * Style : Normal + * Variation : Normal + */ + public static Typeface getNormalTypeface(Context context) { + return TypefaceCache.getTypeface(context, + TypefaceCache.FAMILY_OPEN_SANS, Typeface.NORMAL, TypefaceCache.VARIATION_NORMAL); + } + + /** + * Font : Open Sans + * Style : Bold + * Variation : Light + */ + public static Typeface getSemiboldTypeface(Context context) { + return TypefaceCache.getTypeface(context, + TypefaceCache.FAMILY_OPEN_SANS, Typeface.BOLD, TypefaceCache.VARIATION_LIGHT); + } + + /** + * Styles a {@link TextView} to display a large title against a dark background. + */ + public static void layoutAsLightTitle(TextView view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_extra_large); + setTextViewAttributes(view, size, R.color.white, getSemiboldTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display a large title against a light background. + */ + public static void layoutAsDarkTitle(TextView view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_extra_large); + setTextViewAttributes(view, size, R.color.grey_dark, getSemiboldTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display medium sized text as a header with sub-elements. + */ + public static void layoutAsSubhead(TextView view) { + int color = view.isEnabled() ? R.color.grey_dark : R.color.grey_lighten_10; + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_large); + setTextViewAttributes(view, size, color, getNormalTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display smaller text. + */ + public static void layoutAsBody1(TextView view) { + int color = view.isEnabled() ? R.color.grey_darken_10 : R.color.grey_lighten_10; + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_medium); + setTextViewAttributes(view, size, color, getNormalTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display smaller text with the orange accent color. + */ + public static void layoutAsBody2(TextView view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_medium); + setTextViewAttributes(view, size, R.color.orange_jazzy, getSemiboldTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display very small helper text. + */ + public static void layoutAsCaption(TextView view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_small); + setTextViewAttributes(view, size, R.color.grey_darken_10, getNormalTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display text in a button. + */ + public static void layoutAsFlatButton(TextView view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_medium); + setTextViewAttributes(view, size, R.color.blue_medium, getSemiboldTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display text in a button. + */ + public static void layoutAsRaisedButton(TextView view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_medium); + setTextViewAttributes(view, size, R.color.white, getSemiboldTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display text in an editable text field. + */ + public static void layoutAsInput(EditText view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_large); + setTextViewAttributes(view, size, R.color.grey_dark, getNormalTypeface(view.getContext())); + view.setHintTextColor(view.getResources().getColor(R.color.grey_lighten_10)); + view.setTextColor(view.getResources().getColor(R.color.grey_dark)); + } + + /** + * Styles a {@link TextView} to display selected numbers in a {@link android.widget.NumberPicker}. + */ + public static void layoutAsNumberPickerSelected(TextView view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_triple_extra_large); + setTextViewAttributes(view, size, R.color.blue_medium, getSemiboldTypeface(view.getContext())); + } + + /** + * Styles a {@link TextView} to display non-selected numbers in a {@link android.widget.NumberPicker}. + */ + public static void layoutAsNumberPickerPeek(TextView view) { + int size = view.getResources().getDimensionPixelSize(R.dimen.text_sz_large); + setTextViewAttributes(view, size, R.color.grey_dark, getNormalTypeface(view.getContext())); + } + + public static void setTextViewAttributes(TextView textView, int size, int colorRes, Typeface typeface) { + textView.setTypeface(typeface); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); + textView.setTextColor(textView.getResources().getColor(colorRes)); + } + + /** + * Gets a locale for the given language code. + */ + public static Locale languageLocale(String languageCode) { + if (TextUtils.isEmpty(languageCode)) return Locale.getDefault(); + + if (languageCode.length() > NO_REGION_LANG_CODE_LEN) { + return new Locale(languageCode.substring(0, NO_REGION_LANG_CODE_LEN), + languageCode.substring(REGION_SUBSTRING_INDEX)); + } + + return new Locale(languageCode); + } + + /** + * Creates a map from language codes to WordPress language IDs. + */ + public static Map generateLanguageMap(Activity activity) { + String[] languageIds = activity.getResources().getStringArray(R.array.lang_ids); + String[] languageCodes = activity.getResources().getStringArray(R.array.language_codes); + + Map languageMap = new HashMap<>(); + for (int i = 0; i < languageIds.length && i < languageCodes.length; ++i) { + languageMap.put(languageCodes[i], languageIds[i]); + } + + return languageMap; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPRestClient.java b/WordPress/src/main/java/org/wordpress/android/util/WPRestClient.java index d82f161e6c3c..4d6d327d49af 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/WPRestClient.java +++ b/WordPress/src/main/java/org/wordpress/android/util/WPRestClient.java @@ -255,6 +255,13 @@ public void getStatsVideoSummary(String siteId, Listener listener, ErrorListener getXL(path, params, listener, errorListener); } + public void getSiteDescription(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("rest/v1.1/sites/%s", siteId); + Map params = new HashMap(); + params.put("fields", "description"); + get(path, params, null, listener, errorListener); + } + /** * This method is for simulating stats APIs using the XL Studio API simulator. It should be removed once the other APIs are implemented. **/ diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/TypefaceCache.java b/WordPress/src/main/java/org/wordpress/android/widgets/TypefaceCache.java index 68e5875535b1..1dd9d6add239 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/TypefaceCache.java +++ b/WordPress/src/main/java/org/wordpress/android/widgets/TypefaceCache.java @@ -16,20 +16,20 @@ public class TypefaceCache { * Merriweather is also available via the "fontFamily" attribute */ - private static final int VARIATION_NORMAL = 0; - private static final int VARIATION_LIGHT = 1; - private static final int VARIATION_DEFAULT = VARIATION_NORMAL; + public static final int VARIATION_NORMAL = 0; + public static final int VARIATION_LIGHT = 1; + public static final int VARIATION_DEFAULT = VARIATION_NORMAL; - private static final int FAMILY_OPEN_SANS = 0; - private static final int FAMILY_MERRIWEATHER = 1; - private static final int FAMILY_DEFAULT = FAMILY_OPEN_SANS; + public static final int FAMILY_OPEN_SANS = 0; + public static final int FAMILY_MERRIWEATHER = 1; + public static final int FAMILY_DEFAULT = FAMILY_OPEN_SANS; private static final Hashtable mTypefaceCache = new Hashtable<>(); public static Typeface getTypeface(Context context) { return getTypeface(context, FAMILY_DEFAULT, Typeface.NORMAL, VARIATION_DEFAULT); } - private static Typeface getTypeface(Context context, + public static Typeface getTypeface(Context context, int family, int fontStyle, int variation) { diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPSwitch.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPSwitch.java new file mode 100644 index 000000000000..c51ac22fce12 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPSwitch.java @@ -0,0 +1,21 @@ +package org.wordpress.android.widgets; + +import android.content.Context; +import android.support.v7.widget.SwitchCompat; +import android.util.AttributeSet; + +public class WPSwitch extends SwitchCompat { + public WPSwitch(Context context) { + super(context); + } + + public WPSwitch(Context context, AttributeSet attrs) { + super(context, attrs); + TypefaceCache.setCustomTypeface(context, this, attrs); + } + + public WPSwitch(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypefaceCache.setCustomTypeface(context, this, attrs); + } +} diff --git a/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java b/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java index 3e5e19a79498..f0b01844633d 100644 --- a/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java +++ b/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java @@ -10,13 +10,10 @@ import com.android.volley.DefaultRetryPolicy; import com.android.volley.NetworkResponse; import com.android.volley.RedirectError; -import com.android.volley.Request; -import com.android.volley.toolbox.JsonObjectRequest; import com.android.volley.toolbox.RequestFuture; import com.android.volley.toolbox.StringRequest; import com.google.gson.Gson; -import org.json.JSONObject; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; import org.wordpress.android.datasets.CommentTable; @@ -25,7 +22,6 @@ import org.wordpress.android.models.Comment; import org.wordpress.android.models.CommentList; import org.wordpress.android.models.FeatureSet; -import org.wordpress.android.networking.WPDelayedHurlStack; import org.wordpress.android.ui.media.MediaGridFragment.Filter; import org.wordpress.android.ui.stats.StatsUtils; import org.wordpress.android.ui.stats.StatsWidgetProvider; @@ -34,20 +30,14 @@ import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.DateTimeUtils; import org.wordpress.android.util.MapUtils; -import org.wordpress.android.util.UrlUtils; import org.wordpress.android.util.helpers.MediaFile; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.StringReader; -import java.net.URL; -import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -59,7 +49,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLHandshakeException; public class ApiHelper { @@ -87,6 +76,8 @@ public static final class Methods { public static final String EDIT_POST = "wp.editPost"; public static final String EDIT_COMMENT = "wp.editComment"; + public static final String SET_OPTIONS = "wp.setOptions"; + public static final String UPLOAD_FILE = "wp.uploadFile"; public static final String WPCOM_GET_FEATURES = "wpcom.getFeatures"; @@ -94,6 +85,10 @@ public static final class Methods { public static final String LIST_METHODS = "system.listMethods"; } + public static final class Params { + public static final String SHOW_SUPPORTED_POST_FORMATS = "show-supported"; + } + public enum ErrorType { NO_ERROR, UNKNOWN_ERROR, INVALID_CURRENT_BLOG, NETWORK_XMLRPC, INVALID_CONTEXT, INVALID_RESULT, NO_UPLOAD_FILES_CAP, CAST_EXCEPTION, TASK_CANCELLED, UNAUTHORIZED @@ -150,7 +145,7 @@ protected Object doInBackground(Blog... blog) { mBlog.getHttppassword()); Object result = null; Object[] params = { mBlog.getRemoteBlogId(), mBlog.getUsername(), - mBlog.getPassword(), "show-supported" }; + mBlog.getPassword(), Params.SHOW_SUPPORTED_POST_FORMATS }; try { result = client.call(ApiHelper.Methods.GET_POST_FORMATS, params); } catch (ClassCastException cce) { diff --git a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClient.java b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClient.java index 530436a08950..3e79ec9b2145 100644 --- a/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClient.java +++ b/WordPress/src/main/java/org/xmlrpc/android/XMLRPCClient.java @@ -338,7 +338,7 @@ private static void consumeHttpEntity(HttpEntity entity) { public void preparePostMethod(String method, Object[] params, File tempFile) throws IOException, XMLRPCException, IllegalArgumentException, IllegalStateException { // prepare POST body - if (method.equals("wp.uploadFile")) { + if (method.equals(ApiHelper.Methods.UPLOAD_FILE)) { if (!tempFile.exists() && !tempFile.mkdirs()) { throw new XMLRPCException("Path to file could not be created."); } @@ -512,7 +512,7 @@ private Object callXMLRPC(String method, Object[] params, File tempFile) if (!TextUtils.isEmpty(responseString) && responseString.contains("php fatal error") && responseString.contains("bytes exhausted")) { String newErrorMsg; - if (method.equals("wp.uploadFile")) { + if (method.equals(ApiHelper.Methods.UPLOAD_FILE)) { newErrorMsg = "The server doesn't have enough memory to upload this file. You may need to increase the PHP memory limit on your site."; } else { @@ -620,7 +620,7 @@ private boolean checkXMLRPCErrorMessage(Exception exception) { private void deleteTempFile(String method, File tempFile) { if (tempFile != null) { - if ((method.equals("wp.uploadFile"))){ //get rid of the temp file + if ((method.equals(ApiHelper.Methods.UPLOAD_FILE))){ //get rid of the temp file tempFile.delete(); } } diff --git a/WordPress/src/main/res/color/calypso_title_text.xml b/WordPress/src/main/res/color/calypso_title_text.xml new file mode 100644 index 000000000000..ad850970ea5e --- /dev/null +++ b/WordPress/src/main/res/color/calypso_title_text.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/WordPress/src/main/res/color/dialog_compound_button.xml b/WordPress/src/main/res/color/dialog_compound_button.xml new file mode 100644 index 000000000000..31e228846b98 --- /dev/null +++ b/WordPress/src/main/res/color/dialog_compound_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/WordPress/src/main/res/color/related_posts_list_header.xml b/WordPress/src/main/res/color/related_posts_list_header.xml new file mode 100644 index 000000000000..6c730e29ce1e --- /dev/null +++ b/WordPress/src/main/res/color/related_posts_list_header.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/WordPress/src/main/res/color/related_posts_preview_header.xml b/WordPress/src/main/res/color/related_posts_preview_header.xml new file mode 100644 index 000000000000..908d6534ca60 --- /dev/null +++ b/WordPress/src/main/res/color/related_posts_preview_header.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/WordPress/src/main/res/drawable-v21/dialog_info_button_background.xml b/WordPress/src/main/res/drawable-v21/dialog_info_button_background.xml new file mode 100644 index 000000000000..3ba16d9a2fec --- /dev/null +++ b/WordPress/src/main/res/drawable-v21/dialog_info_button_background.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/WordPress/src/main/res/drawable-xhdpi/rppreview1.png b/WordPress/src/main/res/drawable-xhdpi/rppreview1.png new file mode 100644 index 000000000000..dde7ffb4cb96 Binary files /dev/null and b/WordPress/src/main/res/drawable-xhdpi/rppreview1.png differ diff --git a/WordPress/src/main/res/drawable-xhdpi/rppreview2.png b/WordPress/src/main/res/drawable-xhdpi/rppreview2.png new file mode 100644 index 000000000000..ba4ba7a2dcc0 Binary files /dev/null and b/WordPress/src/main/res/drawable-xhdpi/rppreview2.png differ diff --git a/WordPress/src/main/res/drawable-xhdpi/rppreview3.png b/WordPress/src/main/res/drawable-xhdpi/rppreview3.png new file mode 100644 index 000000000000..8dbe594b500f Binary files /dev/null and b/WordPress/src/main/res/drawable-xhdpi/rppreview3.png differ diff --git a/WordPress/src/main/res/drawable-xxhdpi/rppreview1.png b/WordPress/src/main/res/drawable-xxhdpi/rppreview1.png new file mode 100644 index 000000000000..f4d93fef1cda Binary files /dev/null and b/WordPress/src/main/res/drawable-xxhdpi/rppreview1.png differ diff --git a/WordPress/src/main/res/drawable-xxhdpi/rppreview2.png b/WordPress/src/main/res/drawable-xxhdpi/rppreview2.png new file mode 100644 index 000000000000..8a002bdd23d0 Binary files /dev/null and b/WordPress/src/main/res/drawable-xxhdpi/rppreview2.png differ diff --git a/WordPress/src/main/res/drawable-xxhdpi/rppreview3.png b/WordPress/src/main/res/drawable-xxhdpi/rppreview3.png new file mode 100644 index 000000000000..8b1a388f1f26 Binary files /dev/null and b/WordPress/src/main/res/drawable-xxhdpi/rppreview3.png differ diff --git a/WordPress/src/main/res/drawable-xxxhdpi/rppreview1.png b/WordPress/src/main/res/drawable-xxxhdpi/rppreview1.png new file mode 100644 index 000000000000..3a911c3d19bb Binary files /dev/null and b/WordPress/src/main/res/drawable-xxxhdpi/rppreview1.png differ diff --git a/WordPress/src/main/res/drawable-xxxhdpi/rppreview2.png b/WordPress/src/main/res/drawable-xxxhdpi/rppreview2.png new file mode 100644 index 000000000000..e1bbbc588b6e Binary files /dev/null and b/WordPress/src/main/res/drawable-xxxhdpi/rppreview2.png differ diff --git a/WordPress/src/main/res/drawable-xxxhdpi/rppreview3.png b/WordPress/src/main/res/drawable-xxxhdpi/rppreview3.png new file mode 100644 index 000000000000..9e6115609fe7 Binary files /dev/null and b/WordPress/src/main/res/drawable-xxxhdpi/rppreview3.png differ diff --git a/WordPress/src/main/res/drawable/dialog_info_button_background.xml b/WordPress/src/main/res/drawable/dialog_info_button_background.xml new file mode 100644 index 000000000000..e86873f6bc11 --- /dev/null +++ b/WordPress/src/main/res/drawable/dialog_info_button_background.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/WordPress/src/main/res/drawable/preferences_divider.xml b/WordPress/src/main/res/drawable/preferences_divider.xml new file mode 100644 index 000000000000..b912aac177e8 --- /dev/null +++ b/WordPress/src/main/res/drawable/preferences_divider.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/drawable/related_posts_divider.xml b/WordPress/src/main/res/drawable/related_posts_divider.xml new file mode 100644 index 000000000000..16e006514c7d --- /dev/null +++ b/WordPress/src/main/res/drawable/related_posts_divider.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/WordPress/src/main/res/layout/blog_preferences.xml b/WordPress/src/main/res/layout/blog_preferences.xml index 47b8ffa5f03b..5a1266a91536 100644 --- a/WordPress/src/main/res/layout/blog_preferences.xml +++ b/WordPress/src/main/res/layout/blog_preferences.xml @@ -93,7 +93,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> - - + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/detail_list_preference_title.xml b/WordPress/src/main/res/layout/detail_list_preference_title.xml new file mode 100644 index 000000000000..48963d4a0372 --- /dev/null +++ b/WordPress/src/main/res/layout/detail_list_preference_title.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/WordPress/src/main/res/layout/learn_more_pref.xml b/WordPress/src/main/res/layout/learn_more_pref.xml new file mode 100644 index 000000000000..925e1a2e5566 --- /dev/null +++ b/WordPress/src/main/res/layout/learn_more_pref.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/WordPress/src/main/res/layout/learn_more_pref_screen.xml b/WordPress/src/main/res/layout/learn_more_pref_screen.xml new file mode 100644 index 000000000000..e3455db545e1 --- /dev/null +++ b/WordPress/src/main/res/layout/learn_more_pref_screen.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/WordPress/src/main/res/layout/list_editor.xml b/WordPress/src/main/res/layout/list_editor.xml new file mode 100644 index 000000000000..eb5df8698c67 --- /dev/null +++ b/WordPress/src/main/res/layout/list_editor.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/my_site_fragment.xml b/WordPress/src/main/res/layout/my_site_fragment.xml index dc315302f9ff..6ba434eece00 100644 --- a/WordPress/src/main/res/layout/my_site_fragment.xml +++ b/WordPress/src/main/res/layout/my_site_fragment.xml @@ -256,7 +256,9 @@ - + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/related_posts_dialog.xml b/WordPress/src/main/res/layout/related_posts_dialog.xml new file mode 100644 index 000000000000..dab3fa2ddf06 --- /dev/null +++ b/WordPress/src/main/res/layout/related_posts_dialog.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/site_settings_actionbar.xml b/WordPress/src/main/res/layout/site_settings_actionbar.xml new file mode 100644 index 000000000000..7f8d70145a9d --- /dev/null +++ b/WordPress/src/main/res/layout/site_settings_actionbar.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/WordPress/src/main/res/layout/toolbar.xml b/WordPress/src/main/res/layout/toolbar.xml index 2027c8bcd7c8..a1344d3d0cb1 100644 --- a/WordPress/src/main/res/layout/toolbar.xml +++ b/WordPress/src/main/res/layout/toolbar.xml @@ -10,4 +10,18 @@ app:contentInsetLeft="@dimen/toolbar_content_offset" app:contentInsetStart="@dimen/toolbar_content_offset" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" - app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" /> + app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> + + + + diff --git a/WordPress/src/main/res/layout/wp_simple_list_item_1.xml b/WordPress/src/main/res/layout/wp_simple_list_item_1.xml new file mode 100644 index 000000000000..07eb1f6c676e --- /dev/null +++ b/WordPress/src/main/res/layout/wp_simple_list_item_1.xml @@ -0,0 +1,13 @@ + + + diff --git a/WordPress/src/main/res/menu/list_editor.xml b/WordPress/src/main/res/menu/list_editor.xml new file mode 100644 index 000000000000..a882dbe3122f --- /dev/null +++ b/WordPress/src/main/res/menu/list_editor.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/WordPress/src/main/res/values/attrs.xml b/WordPress/src/main/res/values/attrs.xml index 2a9dfa05d2c6..05d1dce014ef 100644 --- a/WordPress/src/main/res/values/attrs.xml +++ b/WordPress/src/main/res/values/attrs.xml @@ -1,5 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml index 90ec870cc987..8f0435641919 100644 --- a/WordPress/src/main/res/values/colors.xml +++ b/WordPress/src/main/res/values/colors.xml @@ -76,6 +76,7 @@ #006b98 + #ff517188 @color/semi_transparent_grey_dark @@ -150,6 +151,9 @@ #d0d5d9 + + #dce1e6 + #4FB769 #A9E9FC diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index a05f61f00044..69014c3473e2 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -93,6 +93,7 @@ 16sp 20sp 24sp + 26sp 24dp 32dp @@ -141,6 +142,22 @@ 3dp + + 20dp + 24dp + 6dp + 6dp + 4dp + 4dp + 4dp + 24dp + 24dp + 24dp + 16dp + 8dp + 36dp + 36dp + 2dp 128dp @@ -201,6 +218,14 @@ 24dp @dimen/menu_item_margin_normal + + 1dp + 24dp + 12dp + 24dp + 12dp + 11sp + 32dp diff --git a/WordPress/src/main/res/values/integers.xml b/WordPress/src/main/res/values/integers.xml index e1f57247f4ac..842ecb672708 100644 --- a/WordPress/src/main/res/values/integers.xml +++ b/WordPress/src/main/res/values/integers.xml @@ -16,4 +16,9 @@ 300 + + 100 + 300 + 365 + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index d34f5b56aa0b..637ff50fac41 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ Select categories Tags (separate tags with commas) Content (tap to add text and media) - Default image width + Default Image Width Password Blogs Account details @@ -72,6 +72,8 @@ Language Disconnect Undo + Never + Unknown Today @@ -176,7 +178,7 @@ URL copied to clipboard - Comments + @string/comments Live preview @@ -221,8 +223,8 @@ Preview Stats Trash - Delete - More + @string/delete + @string/more Back Revert @@ -420,9 +422,311 @@ wp_pref_notification_other_category wp_pref_notification_other_blogs wp_pref_notifications_enabled + wp_pref_notification_types wp_pref_notification_account_emails wp_pref_notification_sights_sounds wp_pref_notification_site_settings + wp_pref_site_screen + wp_pref_site_general + wp_pref_site_account + wp_pref_site_writing + wp_pref_site_title + wp_pref_site_tagline + wp_pref_site_address + wp_pref_site_language + wp_pref_site_visibility + wp_pref_site_username + wp_pref_site_password + wp_pref_site_related_posts + wp_pref_site_location + wp_pref_site_default_category + wp_pref_site_default_format + wp_pref_site_default_this_device + wp_pref_site_default_image_width + wp_pref_site_upload_and_link_image + wp_pref_site_discussion + wp_pref_site_allow_comments + wp_pref_site_allow_comments_nested + wp_pref_site_send_pingbacks + wp_pref_site_send_pingbacks_nested + wp_pref_site_receive_pingbacks + wp_pref_site_receive_pingbacks_nested + wp_pref_site_more_discussion + wp_pref_site_learn_more + wp_pref_site_close_after + wp_pref_site_sort_by + wp_pref_site_threading + wp_pref_site_paging + wp_pref_site_manual_approval + wp_pref_site_identity_required + wp_pref_site_user_account_required + wp_pref_site_whitelist + wp_pref_site_multiple_links + wp_pref_site_moderation_hold + wp_pref_site_blacklist + wp_pref_site_danger + wp_pref_site_start_over + wp_pref_site_delete_site + wp_pref_site_remove_site + + + + + + + Discussion + Privacy + Related Posts + More + Comments + Close after + Oldest first + Newest first + + 1 day + %d days + + + + General + Account + Writing + @string/discussion + Defaults for New Posts + @string/comments + This Device + + + Site Title + Tagline + Address + @string/privacy + @string/language + @string/username + @string/password + Enable Location + Default Category + Default Format + @string/max_thumbnail_px_width + @string/upload_full_size_image + @string/related_posts + @string/more + Allow Comments + Send Pingbacks + Receive Pingbacks + Must include name and email + Users must be signed in + @string/close_after + Sort by + Threading + Paging + Automatically approve + Links in comments + Hold for Moderation + Blacklist + Delete Site + + + Public + Hidden + Private + %d levels + Comments from all users + Comments from known users + @string/none + + Require manual approval for everyone\'s comments. + Automatically approve if the user has a previously approved comment. + Automatically approve everyone\'s comments. + + + Require approval for more than 1 link + Require approval for more than %d links + + + 1 comment per page + %d comments per page + + + Your site is visible to everyone and may be indexed by search engines + Your site is visible to everyone but asks search engines not to index it + Your site is visible only to you and users you approve + + + + + No comments + Known users\' comments + All users + + + @string/oldest_first + @string/newest_first + + + @string/none + Two levels + Three levels + Four levels + Five levels + Six levels + Seven levels + Eight levels + Nine levels + Ten levels + + + @string/site_settings_privacy_public_summary + @string/site_settings_privacy_hidden_summary + @string/site_settings_privacy_private_summary + + + Original Size + 100 + 200 + 300 + 400 + 500 + 600 + 700 + 800 + 900 + 1000 + 1100 + 1200 + 1300 + 1400 + 1500 + 1600 + 1700 + 1800 + 1900 + 2000 + + + + + -1 + 0 + 1 + + + 0 + 1 + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + 1 + 0 + -1 + + + Original Size + 100 + 200 + 300 + 400 + 500 + 600 + 700 + 800 + 900 + 1000 + 1100 + 1200 + 1300 + 1400 + 1500 + 1600 + 1700 + 1800 + 1900 + 2000 + + + + In a few words, explain what this site is about + A short description or catchy phrase to describe your blog + Changing your address is not currently supported + Controls who can see your site + Language this blog is primarily written in + Current user account + Change your password + Automatically add location data to your posts + Sets new post category + Sets new post format + Resizes images in posts to this width + Enable to always upload the fullsize image + Show or hide related posts in reader + View all available Discussion settings + View and change your sites discussion settings + Allow readers to post comments + Attempt to notify any blogs linked to from the article + Allow link notifications from other blogs + Disallow comments after the specified time + Determines the order comments are displayed + Allow nested comments to a certain depth + Display comments in chunks of a specified size + Comments must be manually approved + Comment author must fill out name and e-mail + Users must be registered and logged in to comment + Comment author must have a previously approved comment + Ignores link limit from known users + Comments that match a filter are put in the moderation queue + Comments that match a filter are marked as spam + Removes your site data from the app + + + Show Related Posts + Related Posts displays relevant content from your site below your posts. + Show Header + Show Images + @string/related_posts + Big iPhone/iPad Update Now Available + in \"Mobile\" + The WordPress for Android App Gets a Big Facelift + in \"Apps\" + Upgrade Focus: VideoPress For Weddings + in \"Upgrade\" + + + @string/learn_more + You can override these settings for individual posts. + + + No items + Enter a word or phrase + When a comment contains any of these words in its content, name, URL, e-mail, or IP, it will be held in the moderation queue. You can enter partial words, so \"press\" will match \"WordPress.\" + When a comment contains any of these words in its content, name, URL, e-mail, or IP, it will be marked as spam. You can enter partial words, so \"press\" will match \"WordPress.\" + + + @string/discussion + Close commenting + Comments per page + Break comment threads into multiple pages. + @string/close_after + Automatically close comments on articles. + Automatically close + Require approval for comments that include more than this number of links. + + + Unsupported WordPress version + Disconnected, editing disabled. + + + + Open source licenses @@ -469,7 +773,7 @@ Views Visitors Likes - Comments + @string/comments Visitors and Views @@ -480,7 +784,7 @@ Authors Referrers Videos - Comments + @string/comments Search Terms Publicize Followers @@ -502,7 +806,7 @@ Views Clicks Plays - Comments + @string/comments Followers Since @@ -725,6 +1029,83 @@ Video + + + en_US + az + de + el + es + fr + gd + hi + hu + id + it + ja + ko + nb + nl + pl + ru + sv + th + uz + zh_CN + zh_TW + zh_HK + en_GB + tr + eu + he + pt_BR + ar + ro + mk + en_AU + sr + sk + cy + da + + + + + 1 + 79 + 15 + 17 + 19 + 24 + 476 + 30 + 31 + 33 + 35 + 36 + 40 + 49 + 58 + 62 + 68 + 71 + 458 + 449 + 452 + 482 + 78 + 429 + 29 + 438 + 3 + 61 + 435 + 67 + 64 + 13 + 14 + + New post New media @@ -789,12 +1170,15 @@ This blog is hidden and couldn\'t be loaded. Enable it again in settings and try again. An error occurred while creating the app database. Try reinstalling the app. An error occurred while copying text to clipboard + Couldn\'t retrieve site info + Couldn\'t save site info This post or page was published on another site Add media Couldn\'t load the comment Error downloading image + Related post preview image Manage PIN lock @@ -829,7 +1213,7 @@ Reader Post Tags & Blogs %1$d of %2$d - Comments + @string/comments Followed tags @@ -1034,7 +1418,7 @@ Publish Blog Posts Settings - Comments + @string/comments Switch Site View Admin View Site @@ -1070,6 +1454,13 @@ My Site Me + I would like my site to be private, visible only to users I choose + Discourage search engines from indexing this site + Allow search engines to index this site + + + https://en.support.wordpress.com/privacy-settings + https://en.support.wordpress.com/language-settings Start Date End Date diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml index 8133e03984b0..d9aebf86f76f 100644 --- a/WordPress/src/main/res/values/styles.xml +++ b/WordPress/src/main/res/values/styles.xml @@ -326,6 +326,11 @@ @color/white + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/xml/site_settings.xml b/WordPress/src/main/res/xml/site_settings.xml new file mode 100644 index 000000000000..9af89df83589 --- /dev/null +++ b/WordPress/src/main/res/xml/site_settings.xml @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index b1a7a8a266b1..9410aaa65124 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -107,7 +107,6 @@ public enum Stat { PUSH_AUTHENTICATION_EXPIRED, PUSH_AUTHENTICATION_FAILED, PUSH_AUTHENTICATION_IGNORED, - SETTINGS_LANGUAGE_SELECTION_FORCED, NOTIFICATION_SETTINGS_LIST_OPENED, NOTIFICATION_SETTINGS_STREAMS_OPENED, NOTIFICATION_SETTINGS_DETAILS_OPENED, @@ -119,9 +118,18 @@ public enum Stat { THEMES_CUSTOMIZE_ACCESSED, THEMES_SUPPORT_ACCESSED, THEMES_DETAILS_ACCESSED, + ACCOUNT_SETTINGS_LANGUAGE_SELECTION_FORCED, + SITE_SETTINGS_ACCESSED, + SITE_SETTINGS_ACCESSED_MORE_SETTINGS, + SITE_SETTINGS_LEARN_MORE_CLICKED, + SITE_SETTINGS_LEARN_MORE_LOADED, + SITE_SETTINGS_ADDED_LIST_ITEM, + SITE_SETTINGS_DELETED_LIST_ITEMS, + SITE_SETTINGS_SAVED_REMOTELY, + SITE_SETTINGS_HINT_TOAST_SHOWN, } - private static final List TRACKERS = new ArrayList(); + private static final List TRACKERS = new ArrayList<>(); private AnalyticsTracker() { } diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java index 4e41a64bce72..20e784b29b6a 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerMixpanel.java @@ -16,7 +16,6 @@ import org.wordpress.android.util.AppLog; import java.util.EnumMap; -import java.util.Iterator; import java.util.Map; public class AnalyticsTrackerMixpanel extends Tracker { @@ -35,7 +34,7 @@ public class AnalyticsTrackerMixpanel extends Tracker { public AnalyticsTrackerMixpanel(Context context, String token) throws IllegalArgumentException { super(context); - mAggregatedProperties = new EnumMap(AnalyticsTracker.Stat.class); + mAggregatedProperties = new EnumMap<>(AnalyticsTracker.Stat.class); mMixpanel = MixpanelAPI.getInstance(context, token); } @@ -142,9 +141,8 @@ private void trackMixpanelEventForInstructions(AnalyticsTrackerMixpanelInstructi // Retrieve properties user has already passed in and combine them with the saved properties if (properties != null) { - Iterator iter = properties.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry pairs = (Map.Entry) iter.next(); + for (Object o : properties.entrySet()) { + Map.Entry pairs = (Map.Entry) o; String key = (String) pairs.getKey(); try { Object value = pairs.getValue(); @@ -254,7 +252,7 @@ public void clearAllData() { private AnalyticsTrackerMixpanelInstructionsForStat instructionsForStat( AnalyticsTracker.Stat stat) { - AnalyticsTrackerMixpanelInstructionsForStat instructions = null; + AnalyticsTrackerMixpanelInstructionsForStat instructions; switch (stat) { case APPLICATION_OPENED: instructions = AnalyticsTrackerMixpanelInstructionsForStat. @@ -723,10 +721,6 @@ private AnalyticsTrackerMixpanelInstructionsForStat instructionsForStat( instructions = AnalyticsTrackerMixpanelInstructionsForStat. mixpanelInstructionsForEventName("Push Authentication - Ignored"); break; - case SETTINGS_LANGUAGE_SELECTION_FORCED: - instructions = AnalyticsTrackerMixpanelInstructionsForStat. - mixpanelInstructionsForEventName("Settings - Forced Language Selection"); - break; case NOTIFICATION_SETTINGS_LIST_OPENED: instructions = AnalyticsTrackerMixpanelInstructionsForStat. mixpanelInstructionsForEventName("Notification Settings - Accessed List"); @@ -787,6 +781,50 @@ private AnalyticsTrackerMixpanelInstructionsForStat instructionsForStat( instructions = AnalyticsTrackerMixpanelInstructionsForStat. mixpanelInstructionsForEventName("Themes - Details Accessed"); break; + case ACCOUNT_SETTINGS_LANGUAGE_SELECTION_FORCED: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - Forced Language Selection"); + break; + case SITE_SETTINGS_ACCESSED: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - Site Settings Accessed"); + instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_settings_accessed"); + break; + case SITE_SETTINGS_ACCESSED_MORE_SETTINGS: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - More Settings Accessed"); + instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_more_settings_accessed"); + break; + case SITE_SETTINGS_ADDED_LIST_ITEM: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - Added List Item"); + instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_list_items_added"); + break; + case SITE_SETTINGS_DELETED_LIST_ITEMS: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - Site Deleted List Items"); + instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_list_items_were_deleted"); + break; + case SITE_SETTINGS_HINT_TOAST_SHOWN: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - Preference Hint Shown"); + instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_preference_hints_viewed"); + break; + case SITE_SETTINGS_LEARN_MORE_CLICKED: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - Learn More Clicked"); + instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_learn_more_clicked"); + break; + case SITE_SETTINGS_LEARN_MORE_LOADED: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - Learn More Loaded"); + instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_learn_more_seen"); + break; + case SITE_SETTINGS_SAVED_REMOTELY: + instructions = AnalyticsTrackerMixpanelInstructionsForStat. + mixpanelInstructionsForEventName("Settings - Saved Remotely"); + instructions.setSuperPropertyAndPeoplePropertyToIncrement("number_of_times_settings_updated_remotely"); + break; default: instructions = null; break; @@ -802,6 +840,7 @@ private void incrementPeopleProperty(String property) { } } + @SuppressLint("CommitPrefEdits") private void incrementSuperProperty(String property) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext); int propertyCount = preferences.getInt(property, 0); @@ -858,8 +897,7 @@ private Object propertyForStat(String property, AnalyticsTracker.Stat stat) { } try { - Object valueForProperty = properties.get(property); - return valueForProperty; + return properties.get(property); } catch (JSONException e) { // We are okay with swallowing this exception as the next line will just return a null value } diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java index 4a444ccd9dd3..92611e64dabf 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java @@ -330,9 +330,6 @@ public void track(AnalyticsTracker.Stat stat, Map properties) { case PUSH_AUTHENTICATION_IGNORED: eventName = "push_authentication_ignored"; break; - case SETTINGS_LANGUAGE_SELECTION_FORCED: - eventName = "settings_language_selection_forced"; - break; case NOTIFICATION_SETTINGS_LIST_OPENED: eventName = "notification_settings_list_opened"; break; @@ -372,6 +369,33 @@ public void track(AnalyticsTracker.Stat stat, Map properties) { case THEMES_DETAILS_ACCESSED: eventName = "themes_details_accessed"; break; + case ACCOUNT_SETTINGS_LANGUAGE_SELECTION_FORCED: + eventName = "account_settings_language_selection_forced"; + break; + case SITE_SETTINGS_ACCESSED: + eventName = "site_settings_accessed"; + break; + case SITE_SETTINGS_ACCESSED_MORE_SETTINGS: + eventName = "site_settings_more_settings_accessed"; + break; + case SITE_SETTINGS_ADDED_LIST_ITEM: + eventName = "site_settings_added_list_item"; + break; + case SITE_SETTINGS_DELETED_LIST_ITEMS: + eventName = "site_settings_deleted_list_items"; + break; + case SITE_SETTINGS_HINT_TOAST_SHOWN: + eventName = "site_settings_hint_toast_shown"; + break; + case SITE_SETTINGS_LEARN_MORE_CLICKED: + eventName = "site_settings_learn_more_clicked"; + break; + case SITE_SETTINGS_LEARN_MORE_LOADED: + eventName = "site_settings_learn_more_loaded"; + break; + case SITE_SETTINGS_SAVED_REMOTELY: + eventName = "site_settings_saved_remotely"; + break; default: eventName = null; break; diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java index b73cf3cb7709..e9e3d444669c 100644 --- a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java +++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java @@ -76,6 +76,11 @@ public RestClient getRestClient() { return mRestClient; } + public void getCategories(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/categories", siteId); + get(path, null, null, listener, errorListener); + } + /** * Reply to a comment *

@@ -232,6 +237,18 @@ public void getCurrentTheme(String siteId, Listener listener, ErrorListener erro get(path, listener, errorListener); } + public void getGeneralSettings(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/settings", siteId); + Map params = new HashMap(); + get(path, params, null, listener, errorListener); + } + + public void setGeneralSiteSettings(String siteId, Listener listener, ErrorListener errorListener, + Map params) { + String path = String.format("sites/%s/settings", siteId); + post(path, params, null, listener, errorListener); + } + /** * Make GET request */ diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java index 25ddd2a41226..ab805cedc84c 100644 --- a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java +++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; public class StringUtils { public static String[] mergeStringArrays(String array1[], String array2[]) { @@ -218,6 +219,28 @@ public static String replaceUnicodeSurrogateBlocksWithHTMLEntities(final String return out.toString(); } + /** + * Used to convert a language code ([lc]_[rc] where lc is language code (en, fr, es, etc...) + * and rc is region code (zh-CN, zh-HK, zh-TW, etc...) to a displayable string with the languages + * name. + * + * The input string must be between 2 and 6 characters, inclusive. An empty string is returned + * if that is not the case. + * + * If the input string is recognized by {@link Locale} the result of this method is the given + * + * @return + * non-null + */ + public static String getLanguageString(String languagueCode, Locale displayLocale) { + if (languagueCode == null || languagueCode.length() < 2 || languagueCode.length() > 6) { + return ""; + } + + Locale languageLocale = new Locale(languagueCode.substring(0, 2)); + return languageLocale.getDisplayLanguage(displayLocale) + languagueCode.substring(2); + } + /** * This method ensures that the output String has only * valid XML unicode characters as specified by the