User-selectable contact colors.

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-06-24 13:17:58 -07:00
parent fb9f16ad29
commit 296796eb54
11 changed files with 423 additions and 4 deletions

View File

@ -57,6 +57,10 @@ dependencies {
compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){ compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){
exclude module: 'support-v4' 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.squareup.dagger:dagger:1.2.2'
compile ("com.doomonafireball.betterpickers:library:1.5.3") { compile ("com.doomonafireball.betterpickers:library:1.5.3") {
exclude group: 'com.android.support', module: 'support-v4' exclude group: 'com.android.support', module: 'support-v4'

View File

@ -0,0 +1,24 @@
<!--
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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/color_grid_item_size"
android:layout_height="@dimen/color_grid_item_size">
<ImageView
android:id="@+id/color_view"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:scaleType="fitXY" />
</FrameLayout>

View File

@ -0,0 +1,21 @@
<!--
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.
-->
<android.support.v7.widget.GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/color_grid"
android:padding="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:orientation="horizontal"
app:columnCount="5"/>

View File

@ -190,4 +190,24 @@
<item>2</item> <item>2</item>
</string-array> </string-array>
<string-array name="default_color_choice_values" translatable="false">
<item>#ffE57373</item>
<item>#ffF06292</item>
<item>#ffBA68C8</item>
<item>#ff9575CD</item>
<item>#ff7986CB</item>
<item>#ff64B5F6</item>
<item>#ff4FC3F7</item>
<item>#ff4DD0E1</item>
<item>#FF4DB6AC</item>
<item>#FF81C784</item>
<item>#FFAED581</item>
<item>#FFDCE775</item>
<item>#FFFFD54F</item>
<item>#FFFFB74D</item>
<item>#FFFF8A65</item>
<item>#FFA1887F</item>
<item>#FF90A4AE</item>
</string-array>
</resources> </resources>

View File

@ -135,4 +135,11 @@
<attr name="lockscreen_watermark" format="reference" /> <attr name="lockscreen_watermark" format="reference" />
<attr name="recipient_preference_blocked" format="color"/> <attr name="recipient_preference_blocked" format="color"/>
<declare-styleable name="ColorPreference">
<attr name="itemLayout" format="reference" />
<attr name="choices" format="reference" />
<attr name="numColumns" format="integer" />
</declare-styleable>
</resources> </resources>

View File

@ -24,4 +24,7 @@
<integer name="media_overview_cols">3</integer> <integer name="media_overview_cols">3</integer>
<dimen name="message_details_table_row_pad">10dp</dimen> <dimen name="message_details_table_row_pad">10dp</dimen>
<dimen name="color_grid_extra_padding">32dp</dimen>
<dimen name="color_grid_item_size">48dp</dimen>
</resources> </resources>

View File

@ -618,6 +618,8 @@
<string name="recipient_preferences__ringtone">Ringtone</string> <string name="recipient_preferences__ringtone">Ringtone</string>
<string name="recipient_preferences__vibrate">Vibrate</string> <string name="recipient_preferences__vibrate">Vibrate</string>
<string name="recipient_preferences__block">Block</string> <string name="recipient_preferences__block">Block</string>
<string name="recipient_preferences__color">Color</string>
<string name="recipient_preferences__color_for_this_contact">Color for this contact</string>
<!-- registration_activity --> <!-- registration_activity -->
<string name="registration_activity__textsecure_can_use_instant_messages_to_avoid_sms_charges_when_communicating_with_other_textsecure_users"> <string name="registration_activity__textsecure_can_use_instant_messages_to_avoid_sms_charges_when_communicating_with_other_textsecure_users">

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat <org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:key="pref_key_recipient_mute" android:key="pref_key_recipient_mute"
android:title="@string/recipient_preferences__mute_conversation" android:title="@string/recipient_preferences__mute_conversation"
@ -24,6 +25,17 @@
android:defaultValue="0" android:defaultValue="0"
android:persistent="false"/> android:persistent="false"/>
<org.thoughtcrime.securesms.preferences.ColorPreference
android:key="pref_key_recipient_color"
android:title="@string/recipient_preferences__color"
android:summary="@string/recipient_preferences__color_for_this_contact"
android:defaultValue="@android:color/black"
android:negativeButtonText="@null"
android:positiveButtonText="@null"
android:persistent="false"
app:numColumns="5" />
<Preference android:key="pref_key_recipient_block" <Preference android:key="pref_key_recipient_block"
android:title="@string/recipient_preferences__block" /> android:title="@string/recipient_preferences__block" />

View File

@ -43,7 +43,6 @@ import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener; import android.view.View.OnFocusChangeListener;
import android.view.View.OnKeyListener; import android.view.View.OnKeyListener;
import android.view.ViewStub; import android.view.ViewStub;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.Button; import android.widget.Button;
@ -53,7 +52,6 @@ import android.widget.Toast;
import com.afollestad.materialdialogs.AlertDialogWrapper; import com.afollestad.materialdialogs.AlertDialogWrapper;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.readystatesoftware.systembartint.SystemBarTintManager;
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener; import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.components.AnimatingToggle;
@ -806,7 +804,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onModified(final Recipients recipients) { public void onModified(final Recipients recipients) {
Log.w(TAG, "onModified()");
titleView.post(new Runnable() { titleView.post(new Runnable() {
@Override @Override
public void run() { public void run() {

View File

@ -18,6 +18,7 @@ import android.support.v4.app.Fragment;
import android.support.v4.preference.PreferenceFragment; import android.support.v4.preference.PreferenceFragment;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
@ -28,11 +29,13 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState; import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.preferences.ColorPreference;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.whispersystems.libaxolotl.util.guava.Optional;
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements Recipients.RecipientsModifiedListener 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_TONE = "pref_key_recipient_ringtone";
private static final String PREFERENCE_VIBRATE = "pref_key_recipient_vibrate"; 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_BLOCK = "pref_key_recipient_block";
private static final String PREFERENCE_COLOR = "pref_key_recipient_color";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@ -156,6 +160,8 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
.setOnPreferenceClickListener(new MuteClickedListener()); .setOnPreferenceClickListener(new MuteClickedListener());
this.findPreference(PREFERENCE_BLOCK) this.findPreference(PREFERENCE_BLOCK)
.setOnPreferenceClickListener(new BlockClickedListener()); .setOnPreferenceClickListener(new BlockClickedListener());
this.findPreference(PREFERENCE_COLOR)
.setOnPreferenceChangeListener(new ColorChangeListener());
} }
@Override @Override
@ -174,6 +180,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED); CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED);
RingtonePreference ringtonePreference = (RingtonePreference) this.findPreference(PREFERENCE_TONE); RingtonePreference ringtonePreference = (RingtonePreference) this.findPreference(PREFERENCE_TONE);
ListPreference vibratePreference = (ListPreference) this.findPreference(PREFERENCE_VIBRATE); ListPreference vibratePreference = (ListPreference) this.findPreference(PREFERENCE_VIBRATE);
ColorPreference colorPreference = (ColorPreference) this.findPreference(PREFERENCE_COLOR);
Preference blockPreference = this.findPreference(PREFERENCE_BLOCK); Preference blockPreference = this.findPreference(PREFERENCE_BLOCK);
mutePreference.setChecked(recipients.isMuted()); mutePreference.setChecked(recipients.isMuted());
@ -199,6 +206,14 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
vibratePreference.setValueIndex(2); 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()) { if (!recipients.isSingleRecipient() || recipients.isGroupRecipient()) {
blockPreference.setEnabled(false); blockPreference.setEnabled(false);
} else { } 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<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientPreferenceDatabase(getActivity())
.setColor(recipients, value);
return null;
}
}.execute();
}
return true;
}
}
private class MuteClickedListener implements Preference.OnPreferenceClickListener { private class MuteClickedListener implements Preference.OnPreferenceClickListener {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {

View File

@ -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);
}
}
}