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.text.TextUtils;
30 import android.util.AttributeSet;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.TextView;
35 
36 import com.android.contacts.GroupMetaDataLoader;
37 import com.android.contacts.R;
38 import com.android.contacts.common.model.account.AccountType;
39 import com.android.contacts.common.model.account.AccountType.EditType;
40 import com.android.contacts.common.model.dataitem.DataKind;
41 import com.android.contacts.common.model.RawContactDelta;
42 import com.android.contacts.common.model.ValuesDelta;
43 import com.android.contacts.common.model.RawContactModifier;
44 
45 import com.google.common.base.Objects;
46 
47 import java.util.ArrayList;
48 
49 /**
50  * Custom view that provides all the editor interaction for a specific
51  * {@link Contacts} represented through an {@link RawContactDelta}. Callers can
52  * reuse this view and quickly rebuild its contents through
53  * {@link #setState(RawContactDelta, AccountType, ViewIdGenerator)}.
54  * <p>
55  * Internal updates are performed against {@link ValuesDelta} so that the
56  * source {@link RawContact} can be swapped out. Any state-based changes, such as
57  * adding {@link Data} rows or changing {@link EditType}, are performed through
58  * {@link RawContactModifier} to ensure that {@link AccountType} are enforced.
59  */
60 public class RawContactEditorView extends BaseRawContactEditorView {
61     private static final String KEY_SUPER_INSTANCE_STATE = "superInstanceState";
62 
63     private LayoutInflater mInflater;
64 
65     private StructuredNameEditorView mName;
66     private PhoneticNameEditorView mPhoneticName;
67     private TextFieldsEditorView mNickName;
68 
69     private GroupMembershipView mGroupMembershipView;
70 
71     private ViewGroup mFields;
72 
73     private View mAccountSelector;
74     private TextView mAccountSelectorTypeTextView;
75     private TextView mAccountSelectorNameTextView;
76 
77     private View mAccountHeader;
78     private TextView mAccountHeaderTypeTextView;
79     private TextView mAccountHeaderNameTextView;
80 
81     private long mRawContactId = -1;
82     private boolean mAutoAddToDefaultGroup = true;
83     private Cursor mGroupMetaData;
84     private DataKind mGroupMembershipKind;
85     private RawContactDelta mState;
86 
RawContactEditorView(Context context)87     public RawContactEditorView(Context context) {
88         super(context);
89     }
90 
RawContactEditorView(Context context, AttributeSet attrs)91     public RawContactEditorView(Context context, AttributeSet attrs) {
92         super(context, attrs);
93     }
94 
95     @Override
setEnabled(boolean enabled)96     public void setEnabled(boolean enabled) {
97         super.setEnabled(enabled);
98 
99         View view = getPhotoEditor();
100         if (view != null) {
101             view.setEnabled(enabled);
102         }
103 
104         if (mName != null) {
105             mName.setEnabled(enabled);
106         }
107 
108         if (mPhoneticName != null) {
109             mPhoneticName.setEnabled(enabled);
110         }
111 
112         if (mFields != null) {
113             int count = mFields.getChildCount();
114             for (int i = 0; i < count; i++) {
115                 mFields.getChildAt(i).setEnabled(enabled);
116             }
117         }
118 
119         if (mGroupMembershipView != null) {
120             mGroupMembershipView.setEnabled(enabled);
121         }
122     }
123 
124     @Override
onFinishInflate()125     protected void onFinishInflate() {
126         super.onFinishInflate();
127 
128         mInflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
129 
130         mName = (StructuredNameEditorView)findViewById(R.id.edit_name);
131         mName.setDeletable(false);
132 
133         mPhoneticName = (PhoneticNameEditorView)findViewById(R.id.edit_phonetic_name);
134         mPhoneticName.setDeletable(false);
135 
136         mNickName = (TextFieldsEditorView)findViewById(R.id.edit_nick_name);
137 
138         mFields = (ViewGroup)findViewById(R.id.sect_fields);
139 
140         mAccountHeader = findViewById(R.id.account_header_container);
141         mAccountHeaderTypeTextView = (TextView) findViewById(R.id.account_type);
142         mAccountHeaderNameTextView = (TextView) findViewById(R.id.account_name);
143 
144         mAccountSelector = findViewById(R.id.account_selector_container);
145         mAccountSelectorTypeTextView = (TextView) findViewById(R.id.account_type_selector);
146         mAccountSelectorNameTextView = (TextView) findViewById(R.id.account_name_selector);
147     }
148 
149     @Override
onSaveInstanceState()150     protected Parcelable onSaveInstanceState() {
151         Bundle bundle = new Bundle();
152         // super implementation of onSaveInstanceState returns null
153         bundle.putParcelable(KEY_SUPER_INSTANCE_STATE, super.onSaveInstanceState());
154         return bundle;
155     }
156 
157     @Override
onRestoreInstanceState(Parcelable state)158     protected void onRestoreInstanceState(Parcelable state) {
159         if (state instanceof Bundle) {
160             Bundle bundle = (Bundle) state;
161             super.onRestoreInstanceState(bundle.getParcelable(KEY_SUPER_INSTANCE_STATE));
162             return;
163         }
164         super.onRestoreInstanceState(state);
165     }
166 
167     /**
168      * Set the internal state for this view, given a current
169      * {@link RawContactDelta} state and the {@link AccountType} that
170      * apply to that state.
171      */
172     @Override
setState(RawContactDelta state, AccountType type, ViewIdGenerator vig, boolean isProfile)173     public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,
174             boolean isProfile) {
175 
176         mState = state;
177 
178         // Remove any existing sections
179         mFields.removeAllViews();
180 
181         // Bail if invalid state or account type
182         if (state == null || type == null) return;
183 
184         setId(vig.getId(state, null, null, ViewIdGenerator.NO_VIEW_INDEX));
185 
186         // Make sure we have a StructuredName
187         RawContactModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE);
188 
189         mRawContactId = state.getRawContactId();
190 
191         // Fill in the account info
192         if (isProfile) {
193             String accountName = state.getAccountName();
194             if (TextUtils.isEmpty(accountName)) {
195                 mAccountHeaderNameTextView.setVisibility(View.GONE);
196                 mAccountHeaderTypeTextView.setText(R.string.local_profile_title);
197             } else {
198                 CharSequence accountType = type.getDisplayLabel(mContext);
199                 mAccountHeaderTypeTextView.setText(mContext.getString(R.string.external_profile_title,
200                         accountType));
201                 mAccountHeaderNameTextView.setText(accountName);
202             }
203         } else {
204             String accountName = state.getAccountName();
205             CharSequence accountType = type.getDisplayLabel(mContext);
206             if (TextUtils.isEmpty(accountType)) {
207                 accountType = mContext.getString(R.string.account_phone);
208             }
209             if (!TextUtils.isEmpty(accountName)) {
210                 mAccountHeaderNameTextView.setVisibility(View.VISIBLE);
211                 mAccountHeaderNameTextView.setText(
212                         mContext.getString(R.string.from_account_format, accountName));
213             } else {
214                 // Hide this view so the other text view will be centered vertically
215                 mAccountHeaderNameTextView.setVisibility(View.GONE);
216             }
217             mAccountHeaderTypeTextView.setText(
218                     mContext.getString(R.string.account_type_format, accountType));
219         }
220         updateAccountHeaderContentDescription();
221 
222         // The account selector and header are both used to display the same information.
223         mAccountSelectorTypeTextView.setText(mAccountHeaderTypeTextView.getText());
224         mAccountSelectorTypeTextView.setVisibility(mAccountHeaderTypeTextView.getVisibility());
225         mAccountSelectorNameTextView.setText(mAccountHeaderNameTextView.getText());
226         mAccountSelectorNameTextView.setVisibility(mAccountHeaderNameTextView.getVisibility());
227         // Showing the account header at the same time as the account selector drop down is
228         // confusing. They should be mutually exclusive.
229         mAccountHeader.setVisibility(mAccountSelector.getVisibility() == View.GONE
230                 ? View.VISIBLE : View.GONE);
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, 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