<commit_before>/*
 * Copyright (C) 2015 The Nevolution Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.oasisfeng.nevo.decorators.wechat;

import android.graphics.Typeface;
import android.os.RemoteException;
import android.support.annotation.ColorRes;
import android.support.v4.app.NotificationCompat;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.StyleSpan;
import android.util.Log;

import com.oasisfeng.android.os.IBundle;
import com.oasisfeng.nevo.INotification;
import com.oasisfeng.nevo.StatusBarNotificationEvo;
import com.oasisfeng.nevo.decorator.NevoDecoratorService;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Example of app-specific decorator
 *
 * TODO: Merge lines from history and active if lines from history are less than expected.
 *
 * Created by Oasis on 2015/6/1.
 */
public class WeChatDecorator extends NevoDecoratorService {

	private static final int KMaxNumLines = 10;
	private static final @ColorRes int PRIMARY_COLOR = 0xFF33B332;

	@Override public void apply(final StatusBarNotificationEvo evolving) throws RemoteException {
		final INotification n = evolving.notification();
		final IBundle extras = n.extras();

		// WeChat use dynamic counter as notification ID, which unfortunately will be reset upon evolving (removal, to be exact) by us,
		// causing all messages combined into one notification. So we split them by re-coding the notification ID by title.
		final CharSequence title = extras.getCharSequence(NotificationCompat.EXTRA_TITLE);
		if (title == null || title.length() == 0) return;
		evolving.setId(calcSplitId(title));		// Split into separate slots

		// Chat history in big content view
		final List<StatusBarNotificationEvo> history = getArchivedNotifications(evolving.getOriginalKey(), 20);
		if (history.isEmpty()) return;

		final List<CharSequence> lines = new ArrayList<>(KMaxNumLines);
		CharSequence text = null; int count = 0, num_text_with_colon = 0; final String redundant_prefix = title.toString() + ": ";
		for (final StatusBarNotificationEvo each : history) {
			final IBundle its_extras = each.notification().extras();
			final CharSequence its_title = its_extras.getCharSequence(NotificationCompat.EXTRA_TITLE);
			if (! title.equals(its_title)) continue;	// Skip other conversations sharing the same key.
			final CharSequence its_text = its_extras.getCharSequence(NotificationCompat.EXTRA_TEXT);
			if (its_text == null) continue;
			final int result = trimAndExtractLeadingCounter(its_text);
			if (result >= 0) {
				count = result & 0xFFFF;
				CharSequence trimmed_text = its_text.subSequence(result >> 16, its_text.length());
				if (trimmed_text.toString().startsWith(redundant_prefix))	// Remove redundant prefix
					trimmed_text = trimmed_text.subSequence(redundant_prefix.length(), trimmed_text.length());
				else if (trimmed_text.toString().indexOf(": ") > 0) num_text_with_colon ++;
				lines.add(text = trimmed_text);
			} else {
				count = 1;
				lines.add(text = its_text);
				if (text.toString().indexOf(": ") > 0) num_text_with_colon ++;
			}
		}
		if (lines.isEmpty()) return;

		extras.putCharSequence(NotificationCompat.EXTRA_TEXT, text);

		if (count > 1) {
			Collections.reverse(lines);			// Latest first, since bottom lines will be trimmed by InboxStyle.
			if (num_text_with_colon == lines.size()) for (int i = 0; i < lines.size(); i++) {	// All lines have colon in text
				final CharSequence line = lines.get(i);											// Let's add bold style to them
				final int pos_colon = line.toString().indexOf(": ");
				if (pos_colon <= 0) continue;
				final SpannableString spanned = SpannableString.valueOf(line);
				spanned.setSpan(new StyleSpan(Typeface.BOLD), 0, pos_colon, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
				lines.set(i, spanned);
			}
			n.setNumber(count);
			extras.putCharSequence(NotificationCompat.EXTRA_TITLE_BIG, title);
			extras.putCharSequenceArray(NotificationCompat.EXTRA_TEXT_LINES, lines.size() > count ? lines.subList(0, count) : lines);
			extras.putString(EXTRA_REBUILD_STYLE, STYLE_INBOX);
		}

		n.setColor(PRIMARY_COLOR);
	}

	/** @return the extracted count in 0xFF range and start position in 0xFF00 range */
	private int trimAndExtractLeadingCounter(final CharSequence text) {
		// Parse and remove the leading "[n]" or [n条/則/…]
		if (text == null || text.length() < 4 || text.charAt(0) != '[') return -1;
		int text_start = 2, count_end;
		while (text.charAt(text_start ++) != ']') if (text_start >= text.length()) return -1;

		try {
			final String num = text.subSequence(1, text_start - 1).toString();	// may contain the suffix "条/則"
			for (count_end = 0; count_end < num.length(); count_end ++) if (! Character.isDigit(num.charAt(count_end))) break;
			if (count_end == 0) return -1;		// Not the expected "unread count"
			final int count = Integer.parseInt(num.substring(0, count_end));
			if (count < 2) return -1;

			return count < 0xFFFF ? (count & 0xFFFF) | ((text_start << 16) & 0xFFFF0000) : 0xFFFF | ((text_start << 16) & 0xFF00);
		} catch (final NumberFormatException ignored) {
			Log.d(TAG, "Failed to parse: " + text);<commit_msg>Notification color should always be updated by WeChatDecorator in any case.<commit_after>/*
 * Copyright (C) 2015 The Nevolution Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.oasisfeng.nevo.decorators.wechat;

import android.graphics.Typeface;
import android.os.RemoteException;
import android.support.annotation.ColorRes;
import android.support.v4.app.NotificationCompat;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.StyleSpan;
import android.util.Log;

import com.oasisfeng.android.os.IBundle;
import com.oasisfeng.nevo.INotification;
import com.oasisfeng.nevo.StatusBarNotificationEvo;
import com.oasisfeng.nevo.decorator.NevoDecoratorService;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Example of app-specific decorator
 *
 * TODO: Merge lines from history and active if lines from history are less than expected.
 *
 * Created by Oasis on 2015/6/1.
 */
public class WeChatDecorator extends NevoDecoratorService {

	private static final int KMaxNumLines = 10;
	private static final @ColorRes int PRIMARY_COLOR = 0xFF33B332;

	@Override public void apply(final StatusBarNotificationEvo evolving) throws RemoteException {
		final INotification n = evolving.notification();
		final IBundle extras = n.extras();
		n.setColor(PRIMARY_COLOR);

		// WeChat use dynamic counter as notification ID, which unfortunately will be reset upon evolving (removal, to be exact) by us,
		// causing all messages combined into one notification. So we split them by re-coding the notification ID by title.
		final CharSequence title = extras.getCharSequence(NotificationCompat.EXTRA_TITLE);
		if (title == null || title.length() == 0) return;
		evolving.setId(calcSplitId(title));		// Split into separate slots

		// Chat history in big content view
		final List<StatusBarNotificationEvo> history = getArchivedNotifications(evolving.getOriginalKey(), 20);
		if (history.isEmpty()) return;

		final List<CharSequence> lines = new ArrayList<>(KMaxNumLines);
		CharSequence text = null; int count = 0, num_text_with_colon = 0; final String redundant_prefix = title.toString() + ": ";
		for (final StatusBarNotificationEvo each : history) {
			final IBundle its_extras = each.notification().extras();
			final CharSequence its_title = its_extras.getCharSequence(NotificationCompat.EXTRA_TITLE);
			if (! title.equals(its_title)) continue;	// Skip other conversations sharing the same key.
			final CharSequence its_text = its_extras.getCharSequence(NotificationCompat.EXTRA_TEXT);
			if (its_text == null) continue;
			final int result = trimAndExtractLeadingCounter(its_text);
			if (result >= 0) {
				count = result & 0xFFFF;
				CharSequence trimmed_text = its_text.subSequence(result >> 16, its_text.length());
				if (trimmed_text.toString().startsWith(redundant_prefix))	// Remove redundant prefix
					trimmed_text = trimmed_text.subSequence(redundant_prefix.length(), trimmed_text.length());
				else if (trimmed_text.toString().indexOf(": ") > 0) num_text_with_colon ++;
				lines.add(text = trimmed_text);
			} else {
				count = 1;
				lines.add(text = its_text);
				if (text.toString().indexOf(": ") > 0) num_text_with_colon ++;
			}
		}
		if (lines.isEmpty()) return;

		extras.putCharSequence(NotificationCompat.EXTRA_TEXT, text);

		if (count > 1) {
			Collections.reverse(lines);			// Latest first, since bottom lines will be trimmed by InboxStyle.
			if (num_text_with_colon == lines.size()) for (int i = 0; i < lines.size(); i++) {	// All lines have colon in text
				final CharSequence line = lines.get(i);											// Let's add bold style to them
				final int pos_colon = line.toString().indexOf(": ");
				if (pos_colon <= 0) continue;
				final SpannableString spanned = SpannableString.valueOf(line);
				spanned.setSpan(new StyleSpan(Typeface.BOLD), 0, pos_colon, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
				lines.set(i, spanned);
			}
			n.setNumber(count);
			extras.putCharSequence(NotificationCompat.EXTRA_TITLE_BIG, title);
			extras.putCharSequenceArray(NotificationCompat.EXTRA_TEXT_LINES, lines.size() > count ? lines.subList(0, count) : lines);
			extras.putString(EXTRA_REBUILD_STYLE, STYLE_INBOX);
		}
	}

	/** @return the extracted count in 0xFF range and start position in 0xFF00 range */
	private int trimAndExtractLeadingCounter(final CharSequence text) {
		// Parse and remove the leading "[n]" or [n条/則/…]
		if (text == null || text.length() < 4 || text.charAt(0) != '[') return -1;
		int text_start = 2, count_end;
		while (text.charAt(text_start ++) != ']') if (text_start >= text.length()) return -1;

		try {
			final String num = text.subSequence(1, text_start - 1).toString();	// may contain the suffix "条/則"
			for (count_end = 0; count_end < num.length(); count_end ++) if (! Character.isDigit(num.charAt(count_end))) break;
			if (count_end == 0) return -1;		// Not the expected "unread count"
			final int count = Integer.parseInt(num.substring(0, count_end));
			if (count < 2) return -1;

			return count < 0xFFFF ? (count & 0xFFFF) | ((text_start << 16) & 0xFFFF0000) : 0xFFFF | ((text_start << 16) & 0xFF00);
		} catch (final NumberFormatException ignored) {
			Log.d(TAG, "Failed to parse: " + text);
{'fraction_non_alphanumeric': 0.1120216197367274, 'fraction_numerical': 0.01255339551913521, 'mean_word_length': 4.413874469089193, 'pattern_counts': {'":': 8, '<': 29, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4770038', 'n_tokens_mistral': 3896, 'n_tokens_neox': 3470, 'n_words': 1033}