1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.contacts.editor;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.os.Bundle;
22 import android.os.Parcelable;
23 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
24 import android.provider.ContactsContract.CommonDataKinds.Nickname;
25 import android.provider.ContactsContract.CommonDataKinds.Photo;
26 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
27 import android.provider.ContactsContract.Contacts;
28 import android.provider.ContactsContract.Data;
29 import android.util.AttributeSet;
30 import android.util.Pair;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.ImageView;
35 import android.widget.LinearLayout;
36 import android.widget.TextView;
37 
38 import com.android.contacts.GroupMetaDataLoader;
39 import com.android.contacts.R;
40 import com.android.contacts.common.model.account.AccountType;
41 import com.android.contacts.common.model.account.AccountType.EditType;
42 import com.android.contacts.common.model.dataitem.DataKind;
43 import com.android.contacts.common.model.RawContactDelta;
44 import com.android.contacts.common.model.ValuesDelta;
45 import com.android.contacts.common.model.RawContactModifier;
46 
47 import com.google.common.base.Objects;
48 
49 import java.util.ArrayList;
50 
51 /**
52  * Custom view that provides all the editor interaction for a specific
53  * {@link Contacts} represented through an {@link RawContactDelta}. Callers can
54  * reuse this view and quickly rebuild its contents through
55  * {@link #setState(RawContactDelta, AccountType, ViewIdGenerator)}.
56  * <p>
57  * Internal updates are performed against {@link ValuesDelta} so that the
58  * source {@link RawContact} can be swapped out. Any state-based changes, such as
59  * adding {@link Data} rows or changing {@link EditType}, are performed through
60  * {@link RawContactModifier} to ensure that {@link AccountType} are enforced.
61  */
62 public class RawContactEditorView extends BaseRawContactEditorView {
63     private static final String KEY_SUPER_INSTANCE_STATE = "superInstanceState";
64 
65     private LayoutInflater mInflater;
66 
67     private StructuredNameEditorView mName;
68     private PhoneticNameEditorView mPhoneticName;
69     private TextFieldsEditorView mNickName;
70 
71     private GroupMembershipView mGroupMembershipView;
72 
73     private ViewGroup mFields;
74 
75     private View mAccountSelector;
76     private TextView mAccountSelectorTypeTextView;
77     private TextView mAccountSelectorNameTextView;
78 
79     private View mAccountHeader;
80     private TextView mAccountHeaderTypeTextView;
81     private TextView mAccountHeaderNameTextView;
82     private ImageView mAccountIconImageView;
83 
84     private long mRawContactId = -1;
85     private boolean mAutoAddToDefaultGroup = true;
86     private Cursor mGroupMetaData;
87     private DataKind mGroupMembershipKind;
88     private RawContactDelta mState;
89 
RawContactEditorView(Context context)90     public RawContactEditorView(Context context) {
91         super(context);
92     }
93 
RawContactEditorView(Context context, AttributeSet attrs)94     public RawContactEditorView(Context context, AttributeSet attrs) {
95         super(context, attrs);
96     }
97 
98     @Override
setEnabled(boolean enabled)99     public void setEnabled(boolean enabled) {
100         super.setEnabled(enabled);
101 
102         View view = getPhotoEditor();
103         if (view != null) {
104             view.setEnabled(enabled);
105         }
106 
107         if (mName != null) {
108             mName.setEnabled(enabled);
109         }
110 
111         if (mPhoneticName != null) {
112             mPhoneticName.setEnabled(enabled);
113         }
114 
115         if (mFields != null) {
116             int count = mFields.getChildCount();
117             for (int i = 0; i < count; i++) {
118                 mFields.getChildAt(i).setEnabled(enabled);
119             }
120         }
121 
122         if (mGroupMembershipView != null) {
123             mGroupMembershipView.setEnabled(enabled);
124         }
125     }
126 
127     @Override
onFinishInflate()128     protected void onFinishInflate() {
129         super.onFinishInflate();
130 
131         mInflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
132 
133         mName = (StructuredNameEditorView)findViewById(R.id.edit_name);
134         mName.setDeletable(false);
135 
136         mPhoneticName = (PhoneticNameEditorView)findViewById(R.id.edit_phonetic_name);
137         mPhoneticName.setDeletable(false);
138 
139         mNickName = (TextFieldsEditorView)findViewById(R.id.edit_nick_name);
140 
141         mFields = (ViewGroup)findViewById(R.id.sect_fields);
142 
143         mAccountHeader = findViewById(R.id.account_header_container);
144         mAccountHeaderTypeTextView = (TextView) findViewById(R.id.account_type);
145         mAccountHeaderNameTextView = (TextView) findViewById(R.id.account_name);
146         mAccountIconImageView = (ImageView) findViewById(android.R.id.icon);
147 
148         // The same header is used by both full editor and read-only editor view. The header is
149         // left-aligned with read-only editor view but is not aligned well with full editor. So we
150         // need to shift the text in the header a little bit for full editor.
151         LinearLayout accountInfoView = (LinearLayout) findViewById(R.id.account_info);
152         final int topBottomPaddingDp = (int) getResources().getDimension(R.dimen
153                 .editor_account_header_expandable_top_bottom_padding);
154         final int leftPaddingDp = (int) getResources().getDimension(R.dimen
155                 .editor_account_header_expandable_left_padding);
156         accountInfoView.setPadding(leftPaddingDp, topBottomPaddingDp, 0, topBottomPaddingDp);
157 
158         mAccountSelector = findViewById(R.id.account_selector_container);
159         mAccountSelectorTypeTextView = (TextView) findViewById(R.id.account_type_selector);
160         mAccountSelectorNameTextView = (TextView) findViewById(R.id.account_name_selector);
161     }
162 
163     @Override
onSaveInstanceState()164     protected Parcelable onSaveInstanceState() {
165         Bundle bundle = new Bundle();
166         // super implementation of onSaveInstanceState returns null
167         bundle.putParcelable(KEY_SUPER_INSTANCE_STATE, super.onSaveInstanceState());
168         return bundle;
169     }
170 
171     @Override
onRestoreInstanceState(Parcelable state)172     protected void onRestoreInstanceState(Parcelable state) {
173         if (state instanceof Bundle) {
174             Bundle bundle = (Bundle) state;
175             super.onRestoreInstanceState(bundle.getParcelable(KEY_SUPER_INSTANCE_STATE));
176             return;
177         }
178         super.onRestoreInstanceState(state);
179     }
180 
181     /**
182      * Set the internal state for this view, given a current
183      * {@link RawContactDelta} state and the {@link AccountType} that
184      * apply to that state.
185      */
186     @Override
setState(RawContactDelta state, AccountType type, ViewIdGenerator vig, boolean isProfile)187     public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,
188             boolean isProfile) {
189 
190         mState = state;
191 
192         // Remove any existing sections
193         mFields.removeAllViews();
194 
195         // Bail if invalid state or account type
196         if (state == null || type == null) return;
197 
198         setId(vig.getId(state, null, null, ViewIdGenerator.NO_VIEW_INDEX));
199 
200         // Make sure we have a StructuredName
201         RawContactModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE);
202 
203         mRawContactId = state.getRawContactId();
204 
205         // Fill in the account info
206         final Pair<String,String> accountInfo = isProfile
207                 ? EditorUiUtils.getLocalAccountInfo(getContext(), state.getAccountName(), type)
208                 : EditorUiUtils.getAccountInfo(getContext(), state.getAccountName(), type);
209         if (accountInfo.first == null) {
210             // Hide this view so the other text view will be centered vertically
211             mAccountHeaderNameTextView.setVisibility(View.GONE);
212         } else {
213             mAccountHeaderNameTextView.setVisibility(View.VISIBLE);
214             mAccountHeaderNameTextView.setText(accountInfo.first);
215         }
216         mAccountHeaderTypeTextView.setText(accountInfo.second);
217         updateAccountHeaderContentDescription();
218 
219         // The account selector and header are both used to display the same information.
220         mAccountSelectorTypeTextView.setText(mAccountHeaderTypeTextView.getText());
221         mAccountSelectorTypeTextView.setVisibility(mAccountHeaderTypeTextView.getVisibility());
222         mAccountSelectorNameTextView.setText(mAccountHeaderNameTextView.getText());
223         mAccountSelectorNameTextView.setVisibility(mAccountHeaderNameTextView.getVisibility());
224         // Showing the account header at the same time as the account selector drop down is
225         // confusing. They should be mutually exclusive.
226         mAccountHeader.setVisibility(mAccountSelector.getVisibility() == View.GONE
227                 ? View.VISIBLE : View.GONE);
228 
229         mAccountIconImageView.setImageDrawable(state.getRawContactAccountType(getContext())
230                 .getDisplayIcon(getContext()));
231 
232         // Show photo editor when supported
233         RawContactModifier.ensureKindExists(state, type, Photo.CONTENT_ITEM_TYPE);
234         setHasPhotoEditor((type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null));
235         getPhotoEditor().setEnabled(isEnabled());
236         mName.setEnabled(isEnabled());
237 
238         mPhoneticName.setEnabled(isEnabled());
239 
240         // Show and hide the appropriate views
241         mFields.setVisibility(View.VISIBLE);
242         mName.setVisibility(View.VISIBLE);
243         mPhoneticName.setVisibility(View.VISIBLE);
244 
245         mGroupMembershipKind = type.getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE);
246         if (mGroupMembershipKind != null) {
247             mGroupMembershipView = (GroupMembershipView)mInflater.inflate(
248                     R.layout.item_group_membership, mFields, false);
249             mGroupMembershipView.setKind(mGroupMembershipKind);
250             mGroupMembershipView.setEnabled(isEnabled());
251         }
252 
253         // Create editor sections for each possible data kind
254         for (DataKind kind : type.getSortedDataKinds()) {
255             // Skip kind of not editable
256             if (!kind.editable) continue;
257 
258             final String mimeType = kind.mimeType;
259             if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
260                 // Handle special case editor for structured name
261                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
262                 mName.setValues(
263                         type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME),
264                         primary, state, false, vig);
265                 mPhoneticName.setValues(
266                         type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME),
267                         primary, state, false, vig);
268                 // It is useful to use Nickname outside of a KindSectionView so that we can treat it
269                 // as a part of StructuredName's fake KindSectionView, even though it uses a
270                 // different CP2 mime-type. We do a bit of extra work below to make this possible.
271                 final DataKind nickNameKind = type.getKindForMimetype(Nickname.CONTENT_ITEM_TYPE);
272                 if (nickNameKind != null) {
273                     ValuesDelta primaryNickNameEntry = state.getPrimaryEntry(nickNameKind.mimeType);
274                     if (primaryNickNameEntry == null) {
275                         primaryNickNameEntry = RawContactModifier.insertChild(state, nickNameKind);
276                     }
277                     mNickName.setValues(nickNameKind, primaryNickNameEntry, state, false, vig);
278                     mNickName.setDeletable(false);
279                 } else {
280                     mPhoneticName.setPadding(0, 0, 0, (int) getResources().getDimension(
281                             R.dimen.editor_padding_between_editor_views));
282                     mNickName.setVisibility(View.GONE);
283                 }
284             } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
285                 // Handle special case editor for photos
286                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
287                 getPhotoEditor().setValues(kind, primary, state, false, vig);
288             } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
289                 if (mGroupMembershipView != null) {
290                     mGroupMembershipView.setState(state);
291                     mFields.addView(mGroupMembershipView);
292                 }
293             } else if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
294                     || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)
295                     || Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
296                 // Don't create fields for each of these mime-types. They are handled specially.
297                 continue;
298             } else {
299                 // Otherwise use generic section-based editors
300                 if (kind.fieldList == null) continue;
301                 final KindSectionView section = (KindSectionView)mInflater.inflate(
302                         R.layout.item_kind_section, mFields, false);
303                 section.setEnabled(isEnabled());
304                 section.setState(kind, state, /* readOnly =*/ false, vig);
305                 mFields.addView(section);
306             }
307         }
308 
309         addToDefaultGroupIfNeeded();
310     }
311 
312     @Override
setGroupMetaData(Cursor groupMetaData)313     public void setGroupMetaData(Cursor groupMetaData) {
314         mGroupMetaData = groupMetaData;
315         addToDefaultGroupIfNeeded();
316         if (mGroupMembershipView != null) {
317             mGroupMembershipView.setGroupMetaData(groupMetaData);
318         }
319     }
320 
setAutoAddToDefaultGroup(boolean flag)321     public void setAutoAddToDefaultGroup(boolean flag) {
322         this.mAutoAddToDefaultGroup = flag;
323     }
324 
325     /**
326      * If automatic addition to the default group was requested (see
327      * {@link #setAutoAddToDefaultGroup}, checks if the raw contact is in any
328      * group and if it is not adds it to the default group (in case of Google
329      * contacts that's "My Contacts").
330      */
addToDefaultGroupIfNeeded()331     private void addToDefaultGroupIfNeeded() {
332         if (!mAutoAddToDefaultGroup || mGroupMetaData == null || mGroupMetaData.isClosed()
333                 || mState == null) {
334             return;
335         }
336 
337         boolean hasGroupMembership = false;
338         ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
339         if (entries != null) {
340             for (ValuesDelta values : entries) {
341                 Long id = values.getGroupRowId();
342                 if (id != null && id.longValue() != 0) {
343                     hasGroupMembership = true;
344                     break;
345                 }
346             }
347         }
348 
349         if (!hasGroupMembership) {
350             long defaultGroupId = getDefaultGroupId();
351             if (defaultGroupId != -1) {
352                 ValuesDelta entry = RawContactModifier.insertChild(mState, mGroupMembershipKind);
353                 if (entry != null) {
354                     entry.setGroupRowId(defaultGroupId);
355                 }
356             }
357         }
358     }
359 
360     /**
361      * Returns the default group (e.g. "My Contacts") for the current raw contact's
362      * account.  Returns -1 if there is no such group.
363      */
getDefaultGroupId()364     private long getDefaultGroupId() {
365         String accountType = mState.getAccountType();
366         String accountName = mState.getAccountName();
367         String accountDataSet = mState.getDataSet();
368         mGroupMetaData.moveToPosition(-1);
369         while (mGroupMetaData.moveToNext()) {
370             String name = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
371             String type = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
372             String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET);
373             if (name.equals(accountName) && type.equals(accountType)
374                     && Objects.equal(dataSet, accountDataSet)) {
375                 long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
376                 if (!mGroupMetaData.isNull(GroupMetaDataLoader.AUTO_ADD)
377                             && mGroupMetaData.getInt(GroupMetaDataLoader.AUTO_ADD) != 0) {
378                     return groupId;
379                 }
380             }
381         }
382         return -1;
383     }
384 
getNameEditor()385     public StructuredNameEditorView getNameEditor() {
386         return mName;
387     }
388 
getPhoneticNameEditor()389     public TextFieldsEditorView getPhoneticNameEditor() {
390         return mPhoneticName;
391     }
392 
getNickNameEditor()393     public TextFieldsEditorView getNickNameEditor() {
394         return mNickName;
395     }
396 
397     @Override
getRawContactId()398     public long getRawContactId() {
399         return mRawContactId;
400     }
401 }
402