From 296796eb544c5292a5595e1efc1119a2add30eee Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 24 Jun 2015 13:17:58 -0700 Subject: [PATCH] User-selectable contact colors. // FREEBIE --- build.gradle | 4 + res/layout/color_preference_item.xml | 24 ++ res/layout/color_preference_items.xml | 21 ++ res/values/arrays.xml | 20 ++ res/values/attrs.xml | 7 + res/values/dimens.xml | 3 + res/values/strings.xml | 2 + res/xml/recipient_preferences.xml | 14 +- .../securesms/ConversationActivity.java | 3 - .../RecipientPreferenceActivity.java | 37 +++ .../preferences/ColorPreference.java | 292 ++++++++++++++++++ 11 files changed, 423 insertions(+), 4 deletions(-) create mode 100644 res/layout/color_preference_item.xml create mode 100644 res/layout/color_preference_items.xml create mode 100644 src/org/thoughtcrime/securesms/preferences/ColorPreference.java diff --git a/build.gradle b/build.gradle index 77917e2267..24e95ae23d 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,10 @@ dependencies { compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){ exclude module: 'support-v4' } + compile ('com.android.support:gridlayout-v7:22.2.0') { + exclude module: 'support-v4' + } + compile 'com.squareup.dagger:dagger:1.2.2' compile ("com.doomonafireball.betterpickers:library:1.5.3") { exclude group: 'com.android.support', module: 'support-v4' diff --git a/res/layout/color_preference_item.xml b/res/layout/color_preference_item.xml new file mode 100644 index 0000000000..fac3e5b219 --- /dev/null +++ b/res/layout/color_preference_item.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/color_preference_items.xml b/res/layout/color_preference_items.xml new file mode 100644 index 0000000000..db1a7f8d93 --- /dev/null +++ b/res/layout/color_preference_items.xml @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 79156fab2c..97cf1c9f9a 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -190,4 +190,24 @@ 2 + + #ffE57373 + #ffF06292 + #ffBA68C8 + #ff9575CD + #ff7986CB + #ff64B5F6 + #ff4FC3F7 + #ff4DD0E1 + #FF4DB6AC + #FF81C784 + #FFAED581 + #FFDCE775 + #FFFFD54F + #FFFFB74D + #FFFF8A65 + #FFA1887F + #FF90A4AE + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 1c3d9b7639..bf69ebc9c2 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -135,4 +135,11 @@ + + + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 1b1865093f..ca2e62ef97 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -24,4 +24,7 @@ 3 10dp + + 32dp + 48dp diff --git a/res/values/strings.xml b/res/values/strings.xml index eada056c18..164bd40268 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -618,6 +618,8 @@ Ringtone Vibrate Block + Color + Color for this contact diff --git a/res/xml/recipient_preferences.xml b/res/xml/recipient_preferences.xml index 4f5dd54cad..8b205d8647 100644 --- a/res/xml/recipient_preferences.xml +++ b/res/xml/recipient_preferences.xml @@ -1,5 +1,6 @@ - + + + + diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index bbfbdff129..9959f2cb84 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -43,7 +43,6 @@ import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.View.OnKeyListener; import android.view.ViewStub; -import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; @@ -53,7 +52,6 @@ import android.widget.Toast; import com.afollestad.materialdialogs.AlertDialogWrapper; import com.google.protobuf.ByteString; -import com.readystatesoftware.systembartint.SystemBarTintManager; import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener; import org.thoughtcrime.securesms.components.AnimatingToggle; @@ -806,7 +804,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void onModified(final Recipients recipients) { - Log.w(TAG, "onModified()"); titleView.post(new Runnable() { @Override public void run() { diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java index 0e26da1d7f..cb96cdfec3 100644 --- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java +++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java @@ -18,6 +18,7 @@ import android.support.v4.app.Fragment; import android.support.v4.preference.PreferenceFragment; import android.support.v7.widget.Toolbar; import android.text.TextUtils; +import android.util.Log; import android.view.MenuItem; import android.view.View; import android.widget.TextView; @@ -28,11 +29,13 @@ import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState; +import org.thoughtcrime.securesms.preferences.ColorPreference; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicTheme; +import org.whispersystems.libaxolotl.util.guava.Optional; public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements Recipients.RecipientsModifiedListener { @@ -44,6 +47,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi private static final String PREFERENCE_TONE = "pref_key_recipient_ringtone"; private static final String PREFERENCE_VIBRATE = "pref_key_recipient_vibrate"; private static final String PREFERENCE_BLOCK = "pref_key_recipient_block"; + private static final String PREFERENCE_COLOR = "pref_key_recipient_color"; private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); @@ -156,6 +160,8 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi .setOnPreferenceClickListener(new MuteClickedListener()); this.findPreference(PREFERENCE_BLOCK) .setOnPreferenceClickListener(new BlockClickedListener()); + this.findPreference(PREFERENCE_COLOR) + .setOnPreferenceChangeListener(new ColorChangeListener()); } @Override @@ -174,6 +180,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED); RingtonePreference ringtonePreference = (RingtonePreference) this.findPreference(PREFERENCE_TONE); ListPreference vibratePreference = (ListPreference) this.findPreference(PREFERENCE_VIBRATE); + ColorPreference colorPreference = (ColorPreference) this.findPreference(PREFERENCE_COLOR); Preference blockPreference = this.findPreference(PREFERENCE_BLOCK); mutePreference.setChecked(recipients.isMuted()); @@ -199,6 +206,14 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi vibratePreference.setValueIndex(2); } + if (recipients.getColor().isPresent()) { + colorPreference.setValue(recipients.getColor().get()); + colorPreference.setEnabled(true); + } else { + colorPreference.setValue(getResources().getColor(R.color.textsecure_primary)); + colorPreference.setEnabled(false); + } + if (!recipients.isSingleRecipient() || recipients.isGroupRecipient()) { blockPreference.setEnabled(false); } else { @@ -267,6 +282,28 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi } } + private class ColorChangeListener implements Preference.OnPreferenceChangeListener { + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final int value = (Integer)newValue; + + if (value != recipients.getColor().get()) { + recipients.setColor(Optional.of(value)); + + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + DatabaseFactory.getRecipientPreferenceDatabase(getActivity()) + .setColor(recipients, value); + return null; + } + }.execute(); + } + return true; + } + } + private class MuteClickedListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(Preference preference) { diff --git a/src/org/thoughtcrime/securesms/preferences/ColorPreference.java b/src/org/thoughtcrime/securesms/preferences/ColorPreference.java new file mode 100644 index 0000000000..9b3597bc94 --- /dev/null +++ b/src/org/thoughtcrime/securesms/preferences/ColorPreference.java @@ -0,0 +1,292 @@ +package org.thoughtcrime.securesms.preferences; + +/* + * Copyright 2013 Google Inc. + * + * 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. + */ + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Bundle; +import android.preference.Preference; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.GridLayout; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.thoughtcrime.securesms.R; + +/** + * A preference that allows the user to choose an application or shortcut. + */ +public class ColorPreference extends Preference { + private int[] mColorChoices = {}; + private int mValue = 0; + private int mItemLayoutId = R.layout.color_preference_item; + private int mNumColumns = 5; + private View mPreviewView; + + public ColorPreference(Context context) { + super(context); + initAttrs(null, 0); + } + + public ColorPreference(Context context, AttributeSet attrs) { + super(context, attrs); + initAttrs(attrs, 0); + } + + public ColorPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initAttrs(attrs, defStyle); + } + + private void initAttrs(AttributeSet attrs, int defStyle) { + TypedArray a = getContext().getTheme().obtainStyledAttributes( + attrs, R.styleable.ColorPreference, defStyle, defStyle); + + try { + mItemLayoutId = a.getResourceId(R.styleable.ColorPreference_itemLayout, mItemLayoutId); + mNumColumns = a.getInteger(R.styleable.ColorPreference_numColumns, mNumColumns); + int choicesResId = a.getResourceId(R.styleable.ColorPreference_choices, + R.array.default_color_choice_values); + if (choicesResId > 0) { + String[] choices = a.getResources().getStringArray(choicesResId); + mColorChoices = new int[choices.length]; + for (int i = 0; i < choices.length; i++) { + mColorChoices[i] = Color.parseColor(choices[i]); + } + } + + } finally { + a.recycle(); + } + + setWidgetLayoutResource(mItemLayoutId); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + mPreviewView = view.findViewById(R.id.color_view); + setColorViewValue(mPreviewView, mValue, false); + } + + public void setValue(int value) { + if (callChangeListener(value)) { + mValue = value; + persistInt(value); + notifyChanged(); + } + } + + @Override + protected void onClick() { + super.onClick(); + + ColorDialogFragment fragment = ColorDialogFragment.newInstance(); + fragment.setPreference(this); + + ((AppCompatActivity) getContext()).getSupportFragmentManager().beginTransaction() + .add(fragment, getFragmentTag()) + .commit(); + } + + @Override + protected void onAttachedToActivity() { + super.onAttachedToActivity(); + + AppCompatActivity activity = (AppCompatActivity) getContext(); + ColorDialogFragment fragment = (ColorDialogFragment) activity + .getSupportFragmentManager().findFragmentByTag(getFragmentTag()); + if (fragment != null) { + // re-bind preference to fragment + fragment.setPreference(this); + } + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getInt(index, 0); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValue(restoreValue ? getPersistedInt(0) : (Integer) defaultValue); + } + + public String getFragmentTag() { + return "color_" + getKey(); + } + + public int getValue() { + return mValue; + } + + public static class ColorDialogFragment extends android.support.v4.app.DialogFragment { + private ColorPreference mPreference; + private GridLayout mColorGrid; + + public ColorDialogFragment() { + } + + public static ColorDialogFragment newInstance() { + return new ColorDialogFragment(); + } + + public void setPreference(ColorPreference preference) { + mPreference = preference; + repopulateItems(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + repopulateItems(); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); + View rootView = layoutInflater.inflate(R.layout.color_preference_items, null); + + mColorGrid = (GridLayout) rootView.findViewById(R.id.color_grid); + mColorGrid.setColumnCount(mPreference.mNumColumns); + repopulateItems(); + + return new AlertDialog.Builder(getActivity()) + .setView(rootView) + .create(); + } + + private void repopulateItems() { + if (mPreference == null || mColorGrid == null) { + return; + } + + Context context = mColorGrid.getContext(); + mColorGrid.removeAllViews(); + for (final int color : mPreference.mColorChoices) { + View itemView = LayoutInflater.from(context) + .inflate(R.layout.color_preference_item, mColorGrid, false); + + setColorViewValue(itemView.findViewById(R.id.color_view), color, + color == mPreference.getValue()); + itemView.setClickable(true); + itemView.setFocusable(true); + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mPreference.setValue(color); + dismiss(); + } + }); + + mColorGrid.addView(itemView); + } + + sizeDialog(); + } + + @Override + public void onStart() { + super.onStart(); + sizeDialog(); + } + + private void sizeDialog() { + if (mPreference == null || mColorGrid == null) { + return; + } + + Dialog dialog = getDialog(); + if (dialog == null) { + return; + } + + final Resources res = mColorGrid.getContext().getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); + + // Can't use Integer.MAX_VALUE here (weird issue observed otherwise on 4.2) + mColorGrid.measure( + View.MeasureSpec.makeMeasureSpec(dm.widthPixels, View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(dm.heightPixels, View.MeasureSpec.AT_MOST)); + int width = mColorGrid.getMeasuredWidth(); + int height = mColorGrid.getMeasuredHeight(); + + int extraPadding = res.getDimensionPixelSize(R.dimen.color_grid_extra_padding); + + width += extraPadding; + height += extraPadding; + + dialog.getWindow().setLayout(width, height); + } + } + + private static void setColorViewValue(View view, int color, boolean selected) { + if (view instanceof ImageView) { + ImageView imageView = (ImageView) view; + Resources res = imageView.getContext().getResources(); + + Drawable currentDrawable = imageView.getDrawable(); + GradientDrawable colorChoiceDrawable; + if (currentDrawable instanceof GradientDrawable) { + // Reuse drawable + colorChoiceDrawable = (GradientDrawable) currentDrawable; + } else { + colorChoiceDrawable = new GradientDrawable(); + colorChoiceDrawable.setShape(GradientDrawable.OVAL); + } + + // Set stroke to dark version of color +// int darkenedColor = Color.rgb( +// Color.red(color) * 192 / 256, +// Color.green(color) * 192 / 256, +// Color.blue(color) * 192 / 256); + + colorChoiceDrawable.setColor(color); +// colorChoiceDrawable.setStroke((int) TypedValue.applyDimension( +// TypedValue.COMPLEX_UNIT_DIP, 2, res.getDisplayMetrics()), darkenedColor); + + Drawable drawable = colorChoiceDrawable; + if (selected) { + BitmapDrawable checkmark = (BitmapDrawable) res.getDrawable(R.drawable.check); + checkmark.setGravity(Gravity.CENTER); + drawable = new LayerDrawable(new Drawable[]{ + colorChoiceDrawable, + checkmark}); + } + + imageView.setImageDrawable(drawable); + + } else if (view instanceof TextView) { + ((TextView) view).setTextColor(color); + } + } +}