Compact editor photo picker 1/2 (E15)
* Added photo selection fragment
* Swap between editor and photo selection fragments in CompactEditorActivity
* Moved PhotoHandler (which receives callbacks from PhotoSourceDialogFragment)
from the editor fragment to CompactEditorActivity since sourcing a photo
happens now from the editor photo view and the action bar when in the
photo selection fragment is visible.
* Extract code to get a bitmap or full size photo from a ValuesDelta in
CompactEditorPhotoView and move it to EditorUtiUtils so that it can be used
on both the photo selection fragment and the photo view
Bug 19697372
Bug 23589603
Change-Id: Iecebca44f505527d0be7a3803cd1d8fd4fef65e0
diff --git a/res/drawable-hdpi/ic_check_circle_googblue_drawable_24dp.png b/res/drawable-hdpi/ic_check_circle_googblue_drawable_24dp.png
new file mode 100644
index 0000000..24d308f
--- /dev/null
+++ b/res/drawable-hdpi/ic_check_circle_googblue_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_check_circle_googblue_drawable24dp.png b/res/drawable-mdpi/ic_check_circle_googblue_drawable24dp.png
new file mode 100644
index 0000000..cea7967
--- /dev/null
+++ b/res/drawable-mdpi/ic_check_circle_googblue_drawable24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_check_circle_googblue_drawable_24dp.png b/res/drawable-xhdpi/ic_check_circle_googblue_drawable_24dp.png
new file mode 100644
index 0000000..5cfcaf5
--- /dev/null
+++ b/res/drawable-xhdpi/ic_check_circle_googblue_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_check_circle_googblue_drawable_24dp.png b/res/drawable-xxhdpi/ic_check_circle_googblue_drawable_24dp.png
new file mode 100644
index 0000000..e50091c
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_check_circle_googblue_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_check_circle_googblue_drawable_24dp.png b/res/drawable-xxxhdpi/ic_check_circle_googblue_drawable_24dp.png
new file mode 100644
index 0000000..c26bd2f
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_check_circle_googblue_drawable_24dp.png
Binary files differ
diff --git a/res/layout/compact_contact_editor_activity.xml b/res/layout/compact_contact_editor_activity.xml
index db847b0..595f381 100644
--- a/res/layout/compact_contact_editor_activity.xml
+++ b/res/layout/compact_contact_editor_activity.xml
@@ -15,8 +15,8 @@
limitations under the License.
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/compact_contact_editor_fragment_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
-
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"/>
diff --git a/res/layout/compact_photo_selection_fragment.xml b/res/layout/compact_photo_selection_fragment.xml
new file mode 100644
index 0000000..354b9f3
--- /dev/null
+++ b/res/layout/compact_photo_selection_fragment.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source 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.
+-->
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/grid_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:numColumns="auto_fit"
+ android:columnWidth="100dp"
+ android:stretchMode="none"
+ android:gravity="center"
+ android:drawSelectorOnTop="true"/>
+
diff --git a/res/layout/compact_photo_selection_item.xml b/res/layout/compact_photo_selection_item.xml
new file mode 100644
index 0000000..6f70c80
--- /dev/null
+++ b/res/layout/compact_photo_selection_item.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/SelectableItem"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="125dp"
+ android:layout_height="125dp"
+ android:adjustViewBounds="true"
+ android:layout_centerInParent="true"
+ android:scaleType="centerCrop" />
+
+ <ImageView
+ android:id="@+id/check"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ android:src="@drawable/ic_check_circle_googblue_drawable_24dp"
+ android:visibility="gone"/>
+
+ <ImageView
+ android:id="@+id/account_type"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"/>
+
+</RelativeLayout>
diff --git a/res/menu/edit_contact_photo.xml b/res/menu/edit_contact_photo.xml
new file mode 100644
index 0000000..725ea8b
--- /dev/null
+++ b/res/menu/edit_contact_photo.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_photo"
+ android:showAsAction="always"
+ android:icon="@drawable/ic_photo_camera_white_24dp"
+ android:title="@string/menu_change_photo"/>
+</menu>
diff --git a/src/com/android/contacts/activities/CompactContactEditorActivity.java b/src/com/android/contacts/activities/CompactContactEditorActivity.java
index 28dd8a5..358740e 100644
--- a/src/com/android/contacts/activities/CompactContactEditorActivity.java
+++ b/src/com/android/contacts/activities/CompactContactEditorActivity.java
@@ -17,19 +17,110 @@
package com.android.contacts.activities;
import com.android.contacts.R;
-import com.android.contacts.editor.CompactContactEditorFragment;
import com.android.contacts.common.activity.RequestPermissionsActivity;
+import com.android.contacts.common.model.RawContactDeltaList;
+import com.android.contacts.detail.PhotoSelectionHandler;
+import com.android.contacts.editor.CompactContactEditorFragment;
+import com.android.contacts.editor.CompactPhotoSelectionFragment;
+import com.android.contacts.editor.PhotoSourceDialogFragment;
+import android.app.FragmentTransaction;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+
/**
* Contact editor with only the most important fields displayed initially.
*/
-public class CompactContactEditorActivity extends ContactEditorBaseActivity {
+public class CompactContactEditorActivity extends ContactEditorBaseActivity implements
+ PhotoSourceDialogFragment.Listener, CompactPhotoSelectionFragment.Listener {
private static final String TAG_COMPACT_EDITOR = "compact_editor";
+ private static final String TAG_PHOTO_SELECTION = "photo_selector";
+
+ private static final String STATE_PHOTO_MODE = "photo_mode";
+ private static final String STATE_IS_PHOTO_SELECTION = "is_photo_selection";
+
+ /**
+ * Displays a PopupWindow with photo edit options.
+ */
+ private final class CompactPhotoSelectionHandler extends PhotoSelectionHandler {
+
+ /**
+ * Receiver of photo edit option callbacks.
+ */
+ private final class CompactPhotoActionListener extends PhotoActionListener {
+
+ @Override
+ public void onRemovePictureChosen() {
+ getEditorFragment().removePhoto();
+ if (mIsPhotoSelection) {
+ showEditorFragment();
+ }
+ }
+
+ @Override
+ public void onPhotoSelected(Uri uri) throws FileNotFoundException {
+ mPhotoUri = uri;
+ getEditorFragment().updatePhoto(uri);
+ if (mIsPhotoSelection) {
+ showEditorFragment();
+ }
+
+ // Re-create the photo handler the next time we need it so that additional photo
+ // selections create a new temp file (and don't hit the one that was just added
+ // to the cache).
+ mPhotoSelectionHandler = null;
+ }
+
+ @Override
+ public Uri getCurrentPhotoUri() {
+ return mPhotoUri;
+ }
+
+ @Override
+ public void onPhotoSelectionDismissed() {
+ if (mIsPhotoSelection) {
+ showEditorFragment();
+ }
+ }
+ }
+
+ private final CompactPhotoActionListener mPhotoActionListener;
+ private final boolean mIsPhotoSelection;
+
+ public CompactPhotoSelectionHandler(int photoMode, boolean isPhotoSelection) {
+ // We pass a null changeAnchorView since we are overriding onClick so that we
+ // can show the photo options in a dialog instead of a ListPopupWindow (which would
+ // be anchored at changeAnchorView).
+
+ // TODO: empty raw contact delta list
+ super(CompactContactEditorActivity.this, /* changeAnchorView =*/ null, photoMode,
+ /* isDirectoryContact =*/ false, new RawContactDeltaList());
+ mPhotoActionListener = new CompactPhotoActionListener();
+ mIsPhotoSelection = isPhotoSelection;
+ }
+
+ @Override
+ public PhotoActionListener getListener() {
+ return mPhotoActionListener;
+ }
+
+ @Override
+ protected void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
+ mPhotoUri = photoUri;
+ startActivityForResult(intent, requestCode);
+ }
+ }
+
+ private CompactPhotoSelectionFragment mPhotoSelectionFragment;
+ private CompactPhotoSelectionHandler mPhotoSelectionHandler;
+ private Uri mPhotoUri;
+ private int mPhotoMode;
+ private boolean mIsPhotoSelection;
@Override
public void onCreate(Bundle savedState) {
@@ -41,19 +132,131 @@
setContentView(R.layout.compact_contact_editor_activity);
- mFragment = (CompactContactEditorFragment) getFragmentManager().findFragmentByTag(
- TAG_COMPACT_EDITOR);
- if (mFragment == null) {
+ if (savedState == null) {
+ // Create the editor and photo selection fragments
mFragment = new CompactContactEditorFragment();
+ mPhotoSelectionFragment = new CompactPhotoSelectionFragment();
getFragmentManager().beginTransaction()
- .add(R.id.compact_contact_editor_fragment_container,
- (CompactContactEditorFragment) mFragment, TAG_COMPACT_EDITOR)
+ .add(R.id.fragment_container, getEditorFragment(), TAG_COMPACT_EDITOR)
+ .add(R.id.fragment_container, mPhotoSelectionFragment, TAG_PHOTO_SELECTION)
+ .hide(mPhotoSelectionFragment)
.commit();
- }
- mFragment.setListener(mFragmentListener);
+ } else {
+ // Restore state
+ mPhotoMode = savedState.getInt(STATE_PHOTO_MODE);
+ mIsPhotoSelection = savedState.getBoolean(STATE_IS_PHOTO_SELECTION);
+ // Show/hide the editor and photo selection fragments (w/o animations)
+ mFragment = (CompactContactEditorFragment) getFragmentManager()
+ .findFragmentByTag(TAG_COMPACT_EDITOR);
+ mPhotoSelectionFragment = (CompactPhotoSelectionFragment) getFragmentManager()
+ .findFragmentByTag(TAG_PHOTO_SELECTION);
+ final FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
+ if (mIsPhotoSelection) {
+ fragmentTransaction.hide(getEditorFragment()).show(mPhotoSelectionFragment);
+ } else {
+ fragmentTransaction.show(getEditorFragment()).hide(mPhotoSelectionFragment);
+ }
+ fragmentTransaction.commit();
+ }
+
+ // Set listeners
+ mFragment.setListener(mFragmentListener);
+ mPhotoSelectionFragment.setListener(this);
+
+ // Load editor data (even if it's hidden)
final String action = getIntent().getAction();
final Uri uri = Intent.ACTION_EDIT.equals(action) ? getIntent().getData() : null;
mFragment.load(action, uri, getIntent().getExtras());
}
+
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(STATE_PHOTO_MODE, mPhotoMode);
+ outState.putBoolean(STATE_IS_PHOTO_SELECTION, mIsPhotoSelection);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ getPhotoSelectionHandler().handlePhotoActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mIsPhotoSelection) {
+ mIsPhotoSelection = false;
+ showEditorFragment();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ /**
+ * Displays photos from all raw contacts, clicking one set it as the super primary photo.
+ */
+ public void selectPhoto(ArrayList<CompactPhotoSelectionFragment.Photo> photos, int photoMode) {
+ mPhotoMode = photoMode;
+ mIsPhotoSelection = true;
+ mPhotoSelectionFragment.setPhotos(photos, photoMode);
+ showPhotoSelectionFragment();
+ }
+
+ /**
+ * Opens a dialog showing options for the user to change their photo (take, choose, or remove
+ * photo).
+ */
+ public void changePhoto(int photoMode) {
+ mPhotoMode = photoMode;
+ mIsPhotoSelection = false;
+ PhotoSourceDialogFragment.show(this, mPhotoMode);
+ }
+
+ private void showPhotoSelectionFragment() {
+ getFragmentManager().beginTransaction()
+ .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out)
+ .hide(getEditorFragment())
+ .show(mPhotoSelectionFragment)
+ .commit();
+ }
+
+ private void showEditorFragment() {
+ getFragmentManager().beginTransaction()
+ .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out)
+ .hide(mPhotoSelectionFragment)
+ .show((CompactContactEditorFragment) mFragment)
+ .commit();
+ }
+
+ @Override
+ public void onRemovePictureChosen() {
+ getPhotoSelectionHandler().getListener().onRemovePictureChosen();
+ }
+
+ @Override
+ public void onTakePhotoChosen() {
+ getPhotoSelectionHandler().getListener().onTakePhotoChosen();
+ }
+
+ @Override
+ public void onPickFromGalleryChosen() {
+ getPhotoSelectionHandler().getListener().onPickFromGalleryChosen();
+ }
+
+ @Override
+ public void onPhotoSelected(CompactPhotoSelectionFragment.Photo photo) {
+ getEditorFragment().setPrimaryPhoto(photo);
+ showEditorFragment();
+ }
+
+ private PhotoSelectionHandler getPhotoSelectionHandler() {
+ if (mPhotoSelectionHandler == null) {
+ mPhotoSelectionHandler = new CompactPhotoSelectionHandler(
+ mPhotoMode, mIsPhotoSelection);
+ }
+ return mPhotoSelectionHandler;
+ }
+
+ private CompactContactEditorFragment getEditorFragment() {
+ return (CompactContactEditorFragment) mFragment;
+ }
}
diff --git a/src/com/android/contacts/editor/CompactContactEditorFragment.java b/src/com/android/contacts/editor/CompactContactEditorFragment.java
index 031805b..c3ecd57 100644
--- a/src/com/android/contacts/editor/CompactContactEditorFragment.java
+++ b/src/com/android/contacts/editor/CompactContactEditorFragment.java
@@ -43,101 +43,17 @@
import android.widget.Toast;
import java.io.FileNotFoundException;
+import java.util.ArrayList;
/**
* Contact editor with only the most important fields displayed initially.
*/
public class CompactContactEditorFragment extends ContactEditorBaseFragment implements
- CompactRawContactsEditorView.Listener, PhotoSourceDialogFragment.Listener {
+ CompactRawContactsEditorView.Listener, CompactPhotoEditorView.Listener {
- private static final String KEY_PHOTO_URI = "photo_uri";
private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id";
private static final String KEY_UPDATED_PHOTOS = "updated_photos";
- /**
- * Displays a PopupWindow with photo edit options.
- */
- final class PhotoHandler extends PhotoSelectionHandler implements View.OnClickListener {
-
- /**
- * Receiver of photo edit option callbacks.
- */
- private final class PhotoListener extends PhotoActionListener {
-
- @Override
- public void onRemovePictureChosen() {
- getContent().setPhoto(/* bitmap =*/ null);
- mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
-
- // Update the mode so the options change if user clicks the photo again
- mPhotoMode = getPhotoMode();
- }
-
- @Override
- public void onPhotoSelected(Uri uri) throws FileNotFoundException {
- final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
- if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
- Log.w(TAG, "Invalid photo selected");
- }
- getContent().setPhoto(bitmap);
-
- // If a new photo was chosen but not yet saved,
- // we need to update the UI immediately
- mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
- getContent().setFullSizePhoto(uri);
-
- // Update the mode so the options change if user clicks the photo again
- mPhotoMode = getPhotoMode();
-
- // Re-create the photo handler so that any additional photo selections create a
- // new temp file (and don't hit the one that was just added to the cache).
- mPhotoHandler = createPhotoHandler();
- }
-
- @Override
- public Uri getCurrentPhotoUri() {
- return mPhotoUri;
- }
-
- @Override
- public void onPhotoSelectionDismissed() {
- }
- }
-
- private PhotoListener mPhotoListener;
- private int mPhotoMode;
-
- public PhotoHandler(Context context, int photoMode, RawContactDeltaList state) {
- // We pass a null changeAnchorView since we are overriding onClick so that we
- // can show the photo options in a dialog instead of a ListPopupWindow (which would
- // be anchored at changeAnchorView).
- super(context, /* changeAnchorView =*/ null, photoMode, /* isDirectoryContact =*/ false,
- state);
- mPhotoListener = new PhotoListener();
- mPhotoMode = photoMode;
- }
-
- @Override
- public void onClick(View view) {
- PhotoSourceDialogFragment.show(CompactContactEditorFragment.this, mPhotoMode);
- }
-
- @Override
- public PhotoActionListener getListener() {
- return mPhotoListener;
- }
-
- @Override
- protected void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
- mPhotoUri = photoUri;
- mStatus = Status.SUB_ACTIVITY;
-
- CompactContactEditorFragment.this.startActivityForResult(intent, requestCode);
- }
- }
-
- private PhotoHandler mPhotoHandler;
- private Uri mPhotoUri;
private long mPhotoRawContactId;
private Bundle mUpdatedPhotos = new Bundle();
@@ -146,7 +62,6 @@
super.onCreate(savedState);
if (savedState != null) {
- mPhotoUri = savedState.getParcelable(KEY_PHOTO_URI);
mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
}
@@ -164,25 +79,12 @@
@Override
public void onSaveInstanceState(Bundle outState) {
- outState.putParcelable(KEY_PHOTO_URI, mPhotoUri);
outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
super.onSaveInstanceState(outState);
}
@Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (mStatus == Status.SUB_ACTIVITY) {
- mStatus = Status.EDITING;
- }
- if (mPhotoHandler != null
- && mPhotoHandler.handlePhotoActivityResult(requestCode, resultCode, data)) {
- return;
- }
- super.onActivityResult(requestCode, resultCode, data);
- }
-
- @Override
public void onStop() {
super.onStop();
@@ -213,13 +115,14 @@
mHasNewContact, mIsUserProfile, mAccountWithDataSet);
// Set up the photo widget
- mPhotoHandler = createPhotoHandler();
+ editorView.setPhotoListener(this);
mPhotoRawContactId = editorView.getPhotoRawContactId();
- if (mUpdatedPhotos.containsKey(String.valueOf(mPhotoRawContactId))) {
- editorView.setFullSizePhoto((Uri) mUpdatedPhotos.getParcelable(
- String.valueOf(mPhotoRawContactId)));
+ // If there is an updated full resolution photo apply it now, this will be the case if
+ // the user selects or takes a new photo, then rotates the device.
+ final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
+ if (uri != null) {
+ editorView.setFullSizePhoto(uri);
}
- editorView.setPhotoHandler(mPhotoHandler);
// The editor is ready now so make it visible
editorView.setEnabled(isEnabled());
@@ -252,37 +155,6 @@
return true;
}
- private PhotoHandler createPhotoHandler() {
- return new PhotoHandler(getActivity(), getPhotoMode(), mState);
- }
-
- private int getPhotoMode() {
- // To determine the options that are available to the user to update their photo
- // (i.e. the photo mode), check if any of the writable raw contacts has a photo set
- Integer photoMode = null;
- boolean hasWritableAccountType = false;
- final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
- for (RawContactDelta rawContactDelta : mState) {
- if (!rawContactDelta.isVisible()) {
- continue;
- }
- final AccountType accountType = rawContactDelta.getAccountType(accountTypes);
- if (accountType.areContactsWritable()) {
- hasWritableAccountType = true;
- if (getContent().isWritablePhotoSet()) {
- photoMode = PhotoActionPopup.Modes.MULTIPLE_WRITE_ABLE_PHOTOS;
- break;
- }
- }
- }
- // If the mode was not set, base it on whether we saw a writable contact or not
- if (photoMode == null) {
- photoMode = hasWritableAccountType
- ? PhotoActionPopup.Modes.NO_PHOTO : PhotoActionPopup.Modes.READ_ONLY_PHOTO;
- }
- return photoMode;
- }
-
@Override
protected View getAggregationAnchorView(long rawContactId) {
return getContent().getAggregationAnchorView();
@@ -314,25 +186,24 @@
mContext.startService(intent);
}
- @Override
- public void onRemovePictureChosen() {
- if (mPhotoHandler != null) {
- mPhotoHandler.getListener().onRemovePictureChosen();
- }
+ public void removePhoto() {
+ getContent().removePhoto();
+ mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
}
- @Override
- public void onTakePhotoChosen() {
- if (mPhotoHandler != null) {
- mPhotoHandler.getListener().onTakePhotoChosen();
+ public void updatePhoto(Uri uri) throws FileNotFoundException {
+ final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
+ if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
+ Toast.makeText(getContext(), R.string.contactPhotoSavedErrorToast,
+ Toast.LENGTH_SHORT).show();
+ return;
}
+ mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
+ getContent().updatePhoto(uri);
}
- @Override
- public void onPickFromGalleryChosen() {
- if (mPhotoHandler != null) {
- mPhotoHandler.getListener().onPickFromGalleryChosen();
- }
+ public void setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo) {
+ getContent().setPrimaryPhoto(photo);
}
@Override
@@ -370,6 +241,35 @@
getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
}
+ @Override
+ public void onPhotoEditorViewClicked() {
+ if (isMultiAccountContact()) {
+ final ArrayList<CompactPhotoSelectionFragment.Photo> photos = getContent().getPhotos();
+ if (!photos.isEmpty()) {
+ // For aggregate contacts, the user may select a new super primary photo from among
+ // the (non-default) raw contact photos, or source a new photo from the ActionBar
+ getEditorActivity().selectPhoto(photos, getPhotoMode());
+ return;
+ }
+ }
+ // For contacts composed of a single writable raw contact, or no raw contacts have photos,
+ // clicking the photo view simply opens the source photo dialog
+ getEditorActivity().changePhoto(getPhotoMode());
+ }
+
+ private int getPhotoMode() {
+ if (getContent().isWritablePhotoSet()) {
+ return isMultiAccountContact()
+ ? PhotoActionPopup.Modes.MULTIPLE_WRITE_ABLE_PHOTOS
+ : PhotoActionPopup.Modes.WRITE_ABLE_PHOTO;
+ }
+ return PhotoActionPopup.Modes.NO_PHOTO;
+ }
+
+ private CompactContactEditorActivity getEditorActivity() {
+ return (CompactContactEditorActivity) getActivity();
+ }
+
private CompactRawContactsEditorView getContent() {
return (CompactRawContactsEditorView) mContent;
}
diff --git a/src/com/android/contacts/editor/CompactPhotoEditorView.java b/src/com/android/contacts/editor/CompactPhotoEditorView.java
index 783e044..1696a12 100644
--- a/src/com/android/contacts/editor/CompactPhotoEditorView.java
+++ b/src/com/android/contacts/editor/CompactPhotoEditorView.java
@@ -18,16 +18,8 @@
import com.android.contacts.R;
import com.android.contacts.common.ContactPhotoManager;
-import com.android.contacts.common.ContactPhotoManager.DefaultImageProvider;
-import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
-import com.android.contacts.common.ContactsUtils;
-import com.android.contacts.common.model.RawContactDelta;
import com.android.contacts.common.model.ValuesDelta;
-import com.android.contacts.common.model.dataitem.DataKind;
-import com.android.contacts.common.util.MaterialColorMapUtils;
import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
-import com.android.contacts.editor.CompactContactEditorFragment.PhotoHandler;
-import com.android.contacts.util.ContactPhotoUtils;
import com.android.contacts.util.SchedulingUtils;
import com.android.contacts.widget.QuickContactImageView;
@@ -35,30 +27,32 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.DisplayPhoto;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
-import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
import android.widget.RelativeLayout;
/**
- * Displays the primary photo.
+ * Displays a photo and calls the host back when the user clicks it.
*/
public class CompactPhotoEditorView extends RelativeLayout implements View.OnClickListener {
- private static final String TAG = CompactContactEditorFragment.TAG;
+ /**
+ * Callbacks for the host of this view.
+ */
+ public interface Listener {
- private ContactPhotoManager mContactPhotoManager;
- private PhotoHandler mPhotoHandler;
+ /**
+ * Invoked when the user wants to change their photo.
+ */
+ void onPhotoEditorViewClicked();
+ }
+
+ private Listener mListener;
private final float mLandscapePhotoRatio;
private final float mPortraitPhotoRatio;
@@ -67,22 +61,21 @@
private final int mActionBarHeight;
private final int mStatusBarHeight;
- private ValuesDelta mValuesDelta;
- private boolean mReadOnly;
- private boolean mIsPhotoSet;
- private MaterialPalette mMaterialPalette;
-
private QuickContactImageView mPhotoImageView;
private View mPhotoIcon;
private View mPhotoIconOverlay;
private View mPhotoTouchInterceptOverlay;
+ private boolean mReadOnly;
+ private boolean mIsNonDefaultPhotoBound;
+
public CompactPhotoEditorView(Context context) {
this(context, null);
}
public CompactPhotoEditorView(Context context, AttributeSet attrs) {
super(context, attrs);
+
mLandscapePhotoRatio = getTypedFloat(R.dimen.quickcontact_landscape_photo_ratio);
mPortraitPhotoRatio = getTypedFloat(R.dimen.editor_portrait_photo_ratio);
mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel);
@@ -106,81 +99,56 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- mContactPhotoManager = ContactPhotoManager.getInstance(getContext());
-
mPhotoImageView = (QuickContactImageView) findViewById(R.id.photo);
mPhotoIcon = findViewById(R.id.photo_icon);
mPhotoIconOverlay = findViewById(R.id.photo_icon_overlay);
mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay);
}
- public void setValues(DataKind dataKind, ValuesDelta valuesDelta,
- RawContactDelta rawContactDelta, boolean readOnly, MaterialPalette materialPalette,
- ViewIdGenerator viewIdGenerator) {
- mValuesDelta = valuesDelta;
- mReadOnly = readOnly;
- mMaterialPalette = materialPalette;
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+ public void setReadOnly(boolean readOnly) {
+ mReadOnly = readOnly;
if (mReadOnly) {
mPhotoIcon.setVisibility(View.GONE);
mPhotoIconOverlay.setVisibility(View.GONE);
} else {
mPhotoTouchInterceptOverlay.setOnClickListener(this);
}
-
- setId(viewIdGenerator.getId(rawContactDelta, dataKind, valuesDelta, /* viewIndex =*/ 0));
-
- setPhoto(valuesDelta);
}
/**
- * Sets the photo bitmap on this view from the given ValuesDelta. Note that the
- * RawContactDelta underlying this view is not modified in any way. Using this method allows
- * you to show one photo (from a read-only contact, for example) and yet have a different
- * raw contact updated when a new photo is set (from the new raw contact created and attached
- * to the read-only contact).
+ * Tries to bind a full size photo or a bitmap loaded from the given ValuesDelta,
+ * and falls back to the default avatar, tinted using the given MaterialPalette (if it's not
+ * null);
*/
- public void setPhoto(ValuesDelta valuesDelta) {
- if (valuesDelta == null) {
- setDefaultPhoto();
- } else {
- final byte[] bytes = valuesDelta.getAsByteArray(Photo.PHOTO);
- if (bytes == null) {
- setDefaultPhoto();
- } else {
- final Bitmap bitmap = BitmapFactory.decodeByteArray(
- bytes, /* offset =*/ 0, bytes.length);
- mPhotoImageView.setImageBitmap(bitmap);
- mIsPhotoSet = true;
- mValuesDelta.setFromTemplate(false);
-
- // Check if we can update to the full size photo immediately
- if (valuesDelta.getAfter() == null
- || valuesDelta.getAfter().get(Photo.PHOTO) == null) {
- // If the user hasn't updated the PHOTO value, then PHOTO_FILE_ID may contain
- // a reference to a larger version of PHOTO that we can bind to the UI.
- // Otherwise, we need to wait for a call to #setFullSizedPhoto() to update
- // our full sized image.
- final Long fileId = valuesDelta.getAsLong(Photo.PHOTO_FILE_ID);
- if (fileId != null) {
- final Uri photoUri = DisplayPhoto.CONTENT_URI.buildUpon()
- .appendPath(fileId.toString()).build();
- setFullSizedPhoto(photoUri);
- }
- }
- }
+ public void setPhoto(ValuesDelta valuesDelta, MaterialPalette materialPalette) {
+ // Check if we can update to the full size photo immediately
+ final Long photoFileId = EditorUiUtils.getPhotoFileId(valuesDelta);
+ if (photoFileId != null) {
+ final Uri photoUri = ContactsContract.DisplayPhoto.CONTENT_URI.buildUpon()
+ .appendPath(photoFileId.toString()).build();
+ setFullSizedPhoto(photoUri);
+ adjustDimensions();
+ return;
}
- if (mIsPhotoSet) {
- // Add background color behind the white photo icon so that it's visible even
- // if the contact photo is white.
- mPhotoIconOverlay.setBackground(new GradientDrawable(
- GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0, 0x88000000}));
- } else {
- setDefaultPhotoTint();
+ // Use the bitmap image from the values delta
+ final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta);
+ if (bitmap != null) {
+ setPhoto(bitmap);
+ adjustDimensions();
+ return;
}
- // Adjust the photo dimensions following the same logic as MultiShrinkScroll.initialize
+ setDefaultPhoto(materialPalette);
+ adjustDimensions();
+ }
+
+ private void adjustDimensions() {
+ // Follow the same logic as MultiShrinkScroll.initialize
SchedulingUtils.doOnPreDraw(this, /* drawNextFrame =*/ false, new Runnable() {
@Override
public void run() {
@@ -190,7 +158,7 @@
photoWidth = (int) (photoHeight * mLandscapePhotoRatio);
} else {
// Make the photo slightly shorter that it is wide
- photoWidth = getWidth();
+ photoWidth = getContentViewWidth();
photoHeight = (int) (photoWidth / mPortraitPhotoRatio);
}
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
@@ -201,6 +169,13 @@
});
}
+ private int getContentViewWidth() {
+ final Activity activity = (Activity) getContext();
+ final DisplayMetrics displayMetrics = new DisplayMetrics();
+ activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ return displayMetrics.widthPixels;
+ }
+
// We're calculating the height the hard way because using the height of the content view
// (found using android.view.Window.ID_ANDROID_CONTENT) with the soft keyboard up when
// going from portrait to landscape mode results in a very small height value.
@@ -213,100 +188,46 @@
}
/**
- * Set the {@link PhotoHandler} to forward clicks (i.e. requests to edit the photo) to.
- */
- public void setPhotoHandler(PhotoHandler photoHandler) {
- mPhotoHandler = photoHandler;
- }
-
- /**
- * Whether a writable {@link Photo} has been set.
+ * Whether a removable, non-default photo is bound to this view.
*/
public boolean isWritablePhotoSet() {
- return mIsPhotoSet && !mReadOnly;
+ return !mReadOnly && mIsNonDefaultPhotoBound;
}
/**
- * Set the given {@link Bitmap} as the photo in the underlying {@link ValuesDelta}
- * and bind a thumbnail to the UI.
+ * Binds the given bitmap.
*/
- public void setPhoto(Bitmap bitmap) {
- if (mReadOnly) {
- Log.w(TAG, "Attempted to set read only photo. Aborting");
- return;
- }
- if (bitmap == null) {
- mValuesDelta.put(ContactsContract.CommonDataKinds.Photo.PHOTO, (byte[]) null);
- setDefaultPhoto();
- return;
- }
+ private void setPhoto(Bitmap bitmap) {
+ mPhotoImageView.setImageBitmap(bitmap);
+ mIsNonDefaultPhotoBound = true;
+ }
- final int thumbnailSize = ContactsUtils.getThumbnailSize(getContext());
- final Bitmap scaledBitmap = Bitmap.createScaledBitmap(
- bitmap, thumbnailSize, thumbnailSize, /* filter =*/ false);
-
- mPhotoImageView.setImageBitmap(scaledBitmap);
- mIsPhotoSet = true;
- mValuesDelta.setFromTemplate(false);
-
- // When the user chooses a new photo mark it as super primary
- mValuesDelta.setSuperPrimary(true);
-
- // Even though high-res photos cannot be saved by passing them via
- // an EntityDeltaList (since they cause the Bundle size limit to be
- // exceeded), we still pass a low-res thumbnail. This simplifies
- // code all over the place, because we don't have to test whether
- // there is a change in EITHER the delta-list OR a changed photo...
- // this way, there is always a change in the delta-list.
- final byte[] compressed = ContactPhotoUtils.compressBitmap(scaledBitmap);
- if (compressed != null) {
- mValuesDelta.setPhoto(compressed);
- }
+ private void setDefaultPhoto(MaterialPalette materialPalette) {
+ EditorUiUtils.setDefaultPhoto(mPhotoImageView, getResources(), materialPalette);
}
/**
- * Show the default "add photo" place holder.
- */
- private void setDefaultPhoto() {
- mPhotoImageView.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact(
- getResources(), /* hires =*/ false, /* defaultImageRequest =*/ null));
- setDefaultPhotoTint();
- mIsPhotoSet = false;
- mValuesDelta.setFromTemplate(true);
- }
-
- private void setDefaultPhotoTint() {
- final int color = mMaterialPalette == null
- ? MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(
- getResources()).mPrimaryColor
- : mMaterialPalette.mPrimaryColor;
- mPhotoImageView.setTint(color);
- }
-
- /**
- * Bind the photo at the given Uri to the UI but do not set the photo on the underlying
- * {@link ValuesDelta}.
+ * Binds a full size photo loaded from the given Uri.
*/
public void setFullSizedPhoto(Uri photoUri) {
- if (photoUri != null) {
- final DefaultImageProvider fallbackToPreviousImage = new DefaultImageProvider() {
- @Override
- public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
- DefaultImageRequest defaultImageRequest) {
- // Before we finish setting the full sized image, don't change the current
- // image that is set in any way.
- }
- };
- mContactPhotoManager.loadPhoto(mPhotoImageView, photoUri,
- mPhotoImageView.getWidth(), /* darkTheme =*/ false, /* isCircular =*/ false,
- /* defaultImageRequest =*/ null, fallbackToPreviousImage);
- }
+ EditorUiUtils.loadPhoto(ContactPhotoManager.getInstance(getContext()),
+ mPhotoImageView, photoUri);
+ mIsNonDefaultPhotoBound = true;
+ }
+
+ /**
+ * Removes the current bound photo bitmap.
+ */
+ public void removePhoto() {
+ mPhotoImageView.setImageBitmap(/* bitmap =*/ null);
+ mIsNonDefaultPhotoBound = false;
+ setDefaultPhoto(/* materialPalette =*/ null);
}
@Override
public void onClick(View view) {
- if (mPhotoHandler != null) {
- mPhotoHandler.onClick(view);
+ if (mListener != null) {
+ mListener.onPhotoEditorViewClicked();
}
}
}
diff --git a/src/com/android/contacts/editor/CompactPhotoSelectionFragment.java b/src/com/android/contacts/editor/CompactPhotoSelectionFragment.java
new file mode 100644
index 0000000..ce868f9
--- /dev/null
+++ b/src/com/android/contacts/editor/CompactPhotoSelectionFragment.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2015 The Android Open Source 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.android.contacts.editor;
+
+import com.android.contacts.R;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.account.AccountType;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+
+import java.util.ArrayList;
+
+/**
+ * Displays {@link Photo}s in a grid and calls back the host when one is clicked.
+ */
+public class CompactPhotoSelectionFragment extends Fragment {
+
+ private static final String STATE_PHOTOS = "photos";
+ private static final String STATE_PHOTO_MODE = "photoMode";
+
+ /**
+ * Callbacks hosts this Fragment.
+ */
+ public interface Listener {
+
+ /**
+ * Invoked when the user wants to change their photo.
+ */
+ void onPhotoSelected(Photo photo);
+ }
+
+ /**
+ * Holds a photo {@link ValuesDelta} and {@link AccountType} information to draw
+ * an account type icon over it.
+ */
+ public static final class Photo implements Parcelable {
+
+ public static final Creator<Photo> CREATOR = new Creator<Photo>() {
+
+ public Photo createFromParcel(Parcel in) {
+ return new Photo(in);
+ }
+
+ public Photo[] newArray(int size) {
+ return new Photo[size];
+ }
+ };
+
+ public Photo() {
+ }
+
+ private Photo(Parcel source) {
+ readFromParcel(source);
+ }
+
+ // From AccountType, everything we need to display the account type icon
+ public int titleRes;
+ public int iconRes;
+ public String syncAdapterPackageName;
+
+ public ValuesDelta valuesDelta;
+
+ /**
+ * Whether the photo is being displayed for the aggregate contact.
+ * This may be because it is marked super primary or it is the one quick contacts picked
+ * randomly to display because none is marked super primary.
+ */
+ public boolean primary;
+
+ public long rawContactId;
+
+ /**
+ * Pointer back to the KindSectionDataList this photo came from.
+ * See {@link CompactRawContactsEditorView#getPhotos}
+ * See {@link CompactRawContactsEditorView#setPrimaryPhoto}
+ */
+ public int kindSectionDataListIndex = -1;
+ public int valuesDeltaListIndex = -1;
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(titleRes);
+ dest.writeInt(iconRes);
+ dest.writeString(syncAdapterPackageName);
+ dest.writeParcelable(valuesDelta, flags);
+ dest.writeInt(primary ? 1 : 0);
+ dest.writeInt(kindSectionDataListIndex);
+ dest.writeInt(valuesDeltaListIndex);
+ }
+
+ private void readFromParcel(Parcel source) {
+ final ClassLoader classLoader = getClass().getClassLoader();
+ titleRes = source.readInt();
+ iconRes = source.readInt();
+ syncAdapterPackageName = source.readString();
+ valuesDelta = source.readParcelable(classLoader);
+ primary = source.readInt() == 1;
+ kindSectionDataListIndex = source.readInt();
+ valuesDeltaListIndex = source.readInt();
+ }
+ }
+
+ private final class PhotoAdapter extends BaseAdapter {
+
+ private final Context mContext;
+ private final LayoutInflater mLayoutInflater;
+
+ public PhotoAdapter() {
+ mContext = getContext();
+ mLayoutInflater = LayoutInflater.from(mContext);
+ }
+
+ @Override
+ public int getCount() {
+ return mPhotos == null ? 0 : mPhotos.size();
+ }
+
+ @Override
+ public Object getItem(int index) {
+ return mPhotos == null ? null : mPhotos.get(index);
+ }
+
+ @Override
+ public long getItemId(int index) {
+ return index;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (mPhotos == null) return null;
+
+ final View photoItemView;
+ if (convertView == null) {
+ photoItemView = mLayoutInflater.inflate(
+ R.layout.compact_photo_selection_item, /* root =*/ null);
+ } else {
+ photoItemView = convertView;
+ }
+
+ final Photo photo = mPhotos.get(position);
+
+ // Bind the photo
+ final ImageView imageView = (ImageView) photoItemView.findViewById(R.id.image);
+ imageView.setImageBitmap(EditorUiUtils.getPhotoBitmap(photo.valuesDelta));
+
+ // Add the account type icon
+ final ImageView accountTypeImageView = (ImageView)
+ photoItemView.findViewById(R.id.account_type);
+ accountTypeImageView.setImageDrawable(AccountType.getDisplayIcon(
+ mContext, photo.titleRes, photo.iconRes, photo.syncAdapterPackageName));
+
+ // Display a check icon over the primary photo
+ final ImageView checkImageView = (ImageView) photoItemView.findViewById(R.id.check);
+ checkImageView.setVisibility(photo.primary ? View.VISIBLE : View.GONE);
+
+ return photoItemView;
+ }
+ }
+
+ private ArrayList<Photo> mPhotos;
+ private int mPhotoMode;
+ private Listener mListener;
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void setPhotos(ArrayList<Photo> photos, int photoMode) {
+ mPhotos = photos;
+ mPhotoMode = photoMode;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mPhotos = savedInstanceState.getParcelableArrayList(STATE_PHOTOS);
+ mPhotoMode = savedInstanceState.getInt(STATE_PHOTO_MODE, 0);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ setHasOptionsMenu(true);
+
+ final PhotoAdapter photoAdapter = new PhotoAdapter();
+
+ final View view = inflater.inflate(R.layout.compact_photo_selection_fragment,
+ container, false);
+ final GridView gridView = (GridView) view.findViewById(R.id.grid_view);
+ gridView.setAdapter(photoAdapter);
+ gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ // Call the host back so it can set the new photo as primary
+ final Photo photo = (Photo) photoAdapter.getItem(position);
+ if (mListener != null) {
+ mListener.onPhotoSelected(photo);
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putParcelableArrayList(STATE_PHOTOS, mPhotos);
+ outState.putInt(STATE_PHOTO_MODE, mPhotoMode);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.edit_contact_photo, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ getActivity().onBackPressed();
+ return true;
+ case R.id.menu_photo:
+ PhotoSourceDialogFragment.show(getActivity(), mPhotoMode);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/editor/CompactRawContactsEditorView.java b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
index e71c814..090ea31 100644
--- a/src/com/android/contacts/editor/CompactRawContactsEditorView.java
+++ b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
@@ -17,6 +17,7 @@
package com.android.contacts.editor;
import com.android.contacts.R;
+import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.model.AccountTypeManager;
import com.android.contacts.common.model.RawContactDelta;
import com.android.contacts.common.model.RawContactDeltaList;
@@ -27,7 +28,7 @@
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.contacts.common.util.AccountsListAdapter;
import com.android.contacts.common.util.MaterialColorMapUtils;
-import com.android.contacts.editor.CompactContactEditorFragment.PhotoHandler;
+import com.android.contacts.util.ContactPhotoUtils;
import com.android.contacts.util.UiClosables;
import android.content.Context;
@@ -62,6 +63,8 @@
import android.widget.ListPopupWindow;
import android.widget.TextView;
+import java.io.File;
+import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -321,6 +324,7 @@
private boolean mIsExpanded;
private long mPhotoRawContactId;
+ private ValuesDelta mPhotoValuesDelta;
public CompactRawContactsEditorView(Context context) {
super(context);
@@ -401,17 +405,17 @@
}
/**
- * Pass through to {@link CompactPhotoEditorView#setPhotoHandler}.
+ * Pass through to {@link CompactPhotoEditorView#setListener}.
*/
- public void setPhotoHandler(PhotoHandler photoHandler) {
- mPhotoView.setPhotoHandler(photoHandler);
+ public void setPhotoListener(CompactPhotoEditorView.Listener listener) {
+ mPhotoView.setListener(listener);
}
- /**
- * Pass through to {@link CompactPhotoEditorView#setPhoto}.
- */
- public void setPhoto(Bitmap bitmap) {
- mPhotoView.setPhoto(bitmap);
+ public void removePhoto() {
+ mPhotoValuesDelta.setFromTemplate(false);
+ mPhotoValuesDelta.put(Photo.PHOTO, (byte[]) null);
+
+ mPhotoView.removePhoto();
}
/**
@@ -421,6 +425,28 @@
mPhotoView.setFullSizedPhoto(photoUri);
}
+ public void updatePhoto(Uri photoUri) {
+ // Even though high-res photos cannot be saved by passing them via
+ // an EntityDeltaList (since they cause the Bundle size limit to be
+ // exceeded), we still pass a low-res thumbnail. This simplifies
+ // code all over the place, because we don't have to test whether
+ // there is a change in EITHER the delta-list OR a changed photo...
+ // this way, there is always a change in the delta-list.
+ mPhotoValuesDelta.setFromTemplate(false);
+ mPhotoValuesDelta.setSuperPrimary(true);
+ try {
+ final byte[] bytes = EditorUiUtils.getCompressedThumbnailBitmapBytes(
+ getContext(), photoUri);
+ if (bytes != null) {
+ mPhotoValuesDelta.setPhoto(bytes);
+ }
+ } catch (FileNotFoundException e) {
+ elog("Failed to get bitmap from photo Uri");
+ }
+
+ mPhotoView.setFullSizedPhoto(photoUri);
+ }
+
/**
* Pass through to {@link CompactPhotoEditorView#isWritablePhotoSet}.
*/
@@ -435,6 +461,77 @@
return mPhotoRawContactId;
}
+ /**
+ * Returns a data holder for every non-default/non-empty photo from each raw contact, whether
+ * the raw contact is writable or not.
+ */
+ public ArrayList<CompactPhotoSelectionFragment.Photo> getPhotos() {
+ final ArrayList<CompactPhotoSelectionFragment.Photo> photos = new ArrayList<>();
+
+ final List<KindSectionData> kindSectionDataList =
+ mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
+ for (int i = 0; i < kindSectionDataList.size(); i++) {
+ final KindSectionData kindSectionData = kindSectionDataList.get(i);
+ final AccountType accountType = kindSectionData.getAccountType();
+ final List<ValuesDelta> valuesDeltaList = kindSectionData.getValuesDeltas();
+ if (valuesDeltaList == null || valuesDeltaList.isEmpty()) continue;
+ for (int j = 0; j < valuesDeltaList.size(); j++) {
+ final ValuesDelta valuesDelta = valuesDeltaList.get(j);
+ final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta);
+ if (bitmap == null) continue;
+
+ final CompactPhotoSelectionFragment.Photo photo =
+ new CompactPhotoSelectionFragment.Photo();
+ photo.titleRes = accountType.titleRes;
+ photo.iconRes = accountType.iconRes;
+ photo.syncAdapterPackageName = accountType.syncAdapterPackageName;
+ photo.valuesDelta = valuesDelta;
+ photo.primary = valuesDelta.isSuperPrimary();
+ photo.kindSectionDataListIndex = i;
+ photo.valuesDeltaListIndex = j;
+ photos.add(photo);
+ }
+ }
+
+ return photos;
+ }
+
+ /**
+ * Marks the raw contact photo given as primary for the aggregate contact and updates the
+ * UI.
+ */
+ public void setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo) {
+ // Unset primary for all other photos
+ final List<KindSectionData> kindSectionDataList =
+ mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
+ for (KindSectionData kindSectionData : kindSectionDataList) {
+ final List<ValuesDelta> valuesDeltaList = kindSectionData.getValuesDeltas();
+ for (ValuesDelta valuesDelta : valuesDeltaList) {
+ valuesDelta.setSuperPrimary(false);
+ }
+ }
+
+ // Find the values delta to mark as primary
+ if (photo.kindSectionDataListIndex < 0
+ || photo.kindSectionDataListIndex >= kindSectionDataList.size()) {
+ wlog("Invalid kind section data list index");
+ return;
+ }
+ final KindSectionData kindSectionData =
+ kindSectionDataList.get(photo.kindSectionDataListIndex);
+ final List<ValuesDelta> valuesDeltaList = kindSectionData.getValuesDeltas();
+ if (photo.valuesDeltaListIndex >= valuesDeltaList.size()) {
+ wlog("Invalid values delta list index");
+ return;
+ }
+ final ValuesDelta valuesDelta = valuesDeltaList.get(photo.valuesDeltaListIndex);
+ valuesDelta.setFromTemplate(false);
+ valuesDelta.setSuperPrimary(true);
+
+ // Update the UI
+ mPhotoView.setPhoto(valuesDelta, mMaterialPalette);
+ }
+
public View getAggregationAnchorView() {
final List<CompactKindSectionView> kindSectionViews = getKindSectionViews(
StructuredName.CONTENT_ITEM_TYPE);
@@ -691,31 +788,50 @@
}
private void addPhotoView() {
- // Get the kind section data and values delta that will back the photo view
- Pair<KindSectionData,ValuesDelta> pair = getPrimaryKindSectionData(mPhotoId);
+ // Get the kind section data and values delta that we will display in the photo view
+ Pair<KindSectionData,ValuesDelta> pair = getPrimaryPhotoKindSectionData(mPhotoId);
if (pair == null) {
wlog("photo: no kind section data parsed");
+ mPhotoView.setReadOnly(true);
return;
}
- final KindSectionData kindSectionData = pair.first;
- final ValuesDelta valuesDelta = pair.second;
- // If we're editing a read-only contact we want to display the photo from the
- // read-only contact in a photo editor backed by the new raw contact
- // that was created.
- if (mHasNewContact) {
- mPhotoRawContactId = mPrimaryRawContactDelta == null
- ? null : mPrimaryRawContactDelta.getRawContactId();
+ // Set the photo view
+ final ValuesDelta primaryValuesDelta = pair.second;
+ mPhotoView.setPhoto(primaryValuesDelta, mMaterialPalette);
+
+ // Find the raw contact ID and values delta that will be written when the photo is edited
+ final KindSectionData primaryKindSectionData = pair.first;
+ if (mHasNewContact && mPrimaryRawContactDelta != null
+ && !primaryKindSectionData.getValuesDeltas().isEmpty()) {
+ // If we're editing a read-only contact we want to display the photo from the
+ // read-only contact in a photo editor view, but update the new raw contact
+ // that was created.
+ mPhotoRawContactId = mPrimaryRawContactDelta.getRawContactId();
+ mPhotoValuesDelta = primaryKindSectionData.getValuesDeltas().get(0);
+ mPhotoView.setReadOnly(false);
+ return;
+ }
+ if (primaryKindSectionData.getAccountType().areContactsWritable() &&
+ !primaryKindSectionData.getValuesDeltas().isEmpty()) {
+ mPhotoRawContactId = primaryKindSectionData.getRawContactDelta().getRawContactId();
+ mPhotoValuesDelta = primaryKindSectionData.getValuesDeltas().get(0);
+ mPhotoView.setReadOnly(false);
+ return;
}
- mPhotoRawContactId = kindSectionData.getRawContactDelta().getRawContactId();
- mPhotoView.setValues(kindSectionData.getDataKind(), valuesDelta,
- kindSectionData.getRawContactDelta(),
- !kindSectionData.getAccountType().areContactsWritable(), mMaterialPalette,
- mViewIdGenerator);
+ final KindSectionData writableKindSectionData = getFirstWritablePhotoKindSectionData();
+ if (writableKindSectionData == null
+ || writableKindSectionData.getValuesDeltas().isEmpty()) {
+ mPhotoView.setReadOnly(true);
+ return;
+ }
+ mPhotoRawContactId = writableKindSectionData.getRawContactDelta().getRawContactId();
+ mPhotoValuesDelta = writableKindSectionData.getValuesDeltas().get(0);
+ mPhotoView.setReadOnly(false);
}
- private Pair<KindSectionData,ValuesDelta> getPrimaryKindSectionData(long id) {
+ private Pair<KindSectionData,ValuesDelta> getPrimaryPhotoKindSectionData(long id) {
final String mimeType = Photo.CONTENT_ITEM_TYPE;
final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
@@ -766,6 +882,17 @@
? new Pair<>(resultKindSectionData, resultValuesDelta) : null;
}
+ private KindSectionData getFirstWritablePhotoKindSectionData() {
+ final String mimeType = Photo.CONTENT_ITEM_TYPE;
+ final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
+ for (KindSectionData kindSectionData : kindSectionDataList) {
+ if (kindSectionData.getAccountType().areContactsWritable()) {
+ return kindSectionData;
+ }
+ }
+ return null;
+ }
+
private void addKindSectionViews() {
// Sort the kinds
final TreeSet<Map.Entry<String,List<KindSectionData>>> entries =
diff --git a/src/com/android/contacts/editor/ContactEditorBaseFragment.java b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
index 2c3b6c9..c5e6afd 100644
--- a/src/com/android/contacts/editor/ContactEditorBaseFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
@@ -743,7 +743,7 @@
// even if they have never added their own information and splitting will create a
// name only contact.
final boolean isSingleReadOnlyContact = mHasNewContact && mState.size() == 2;
- splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile()
+ splitMenu.setVisible(isMultiAccountContact() && !isEditingUserProfile()
&& !isSingleReadOnlyContact);
// Cannot join a user profile
joinMenu.setVisible(!isEditingUserProfile());
@@ -938,6 +938,10 @@
return mNewLocalProfile || mIsUserProfile;
}
+ protected boolean isMultiAccountContact() {
+ return mState.size() > 1;
+ }
+
/**
* Return true if there are any edits to the current contact which need to
* be saved.
diff --git a/src/com/android/contacts/editor/EditorUiUtils.java b/src/com/android/contacts/editor/EditorUiUtils.java
index 106c5e3..0fdae58 100644
--- a/src/com/android/contacts/editor/EditorUiUtils.java
+++ b/src/com/android/contacts/editor/EditorUiUtils.java
@@ -18,8 +18,12 @@
import static android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import static android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import static com.android.contacts.common.util.MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors;
import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
@@ -37,12 +41,24 @@
import android.os.Build;
import android.text.TextUtils;
import android.util.Pair;
+import android.widget.ImageView;
+
import com.android.contacts.R;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageProvider;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.model.ValuesDelta;
import com.android.contacts.common.model.account.AccountType;
import com.android.contacts.common.model.account.GoogleAccountType;
import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
+import com.android.contacts.util.ContactPhotoUtils;
+import com.android.contacts.widget.QuickContactImageView;
+
import com.google.common.collect.Maps;
+import java.io.FileNotFoundException;
import java.util.HashMap;
/**
@@ -219,4 +235,62 @@
private static boolean isNewerThanM(int currentVersion) {
return currentVersion > Build.VERSION_CODES.M;
}
+
+ /** Returns teh {@link Photo#PHOTO_FILE_ID} from the given ValuesDelta. */
+ public static Long getPhotoFileId(ValuesDelta valuesDelta) {
+ if (valuesDelta == null) return null;
+ if (valuesDelta.getAfter() == null || valuesDelta.getAfter().get(Photo.PHOTO) == null) {
+ return valuesDelta.getAsLong(Photo.PHOTO_FILE_ID);
+ }
+ return null;
+ }
+
+ /** Binds the full resolution image at the given Uri to the provided ImageView. */
+ static void loadPhoto(ContactPhotoManager contactPhotoManager, ImageView imageView,
+ Uri photoUri) {
+ final DefaultImageProvider fallbackToPreviousImage = new DefaultImageProvider() {
+ @Override
+ public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
+ DefaultImageRequest defaultImageRequest) {
+ // Before we finish setting the full sized image, don't change the current
+ // image that is set in any way.
+ }
+ };
+ contactPhotoManager.loadPhoto(imageView, photoUri, imageView.getWidth(),
+ /* darkTheme =*/ false, /* isCircular =*/ false,
+ /* defaultImageRequest =*/ null, fallbackToPreviousImage);
+ }
+
+ /** Decodes the Bitmap from the photo bytes from the given ValuesDelta. */
+ public static Bitmap getPhotoBitmap(ValuesDelta valuesDelta) {
+ if (valuesDelta == null) return null;
+ final byte[] bytes = valuesDelta.getAsByteArray(Photo.PHOTO);
+ if (bytes == null) return null;
+ return BitmapFactory.decodeByteArray(bytes, /* offset =*/ 0, bytes.length);
+ }
+
+ /** Binds the default avatar to the given ImageView and tints it to match QuickContacts. */
+ public static void setDefaultPhoto(ImageView imageView , Resources resources,
+ MaterialPalette materialPalette) {
+ // Use the default avatar drawable
+ imageView.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact(
+ resources, /* hires =*/ false, /* defaultImageRequest =*/ null));
+
+ // Tint it to match the quick contacts
+ if (imageView instanceof QuickContactImageView) {
+ ((QuickContactImageView) imageView).setTint(materialPalette == null
+ ? getDefaultPrimaryAndSecondaryColors(resources).mPrimaryColor
+ : materialPalette.mPrimaryColor);
+ }
+ }
+
+ /** Returns compressed bitmap bytes from the given Uri, scaled to the thumbnail dimensions. */
+ public static byte[] getCompressedThumbnailBitmapBytes(Context context, Uri uri)
+ throws FileNotFoundException {
+ final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(context, uri);
+ final int size = ContactsUtils.getThumbnailSize(context);
+ final Bitmap bitmapScaled = Bitmap.createScaledBitmap(
+ bitmap, size, size, /* filter =*/ false);
+ return ContactPhotoUtils.compressBitmap(bitmapScaled);
+ }
}
diff --git a/src/com/android/contacts/editor/KindSectionData.java b/src/com/android/contacts/editor/KindSectionData.java
index 33aeed2..6c33601 100644
--- a/src/com/android/contacts/editor/KindSectionData.java
+++ b/src/com/android/contacts/editor/KindSectionData.java
@@ -27,7 +27,6 @@
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.text.TextUtils;
-import java.util.Collections;
import java.util.List;
/**
diff --git a/src/com/android/contacts/editor/PhotoSourceDialogFragment.java b/src/com/android/contacts/editor/PhotoSourceDialogFragment.java
index e16fdae..4b502e5 100644
--- a/src/com/android/contacts/editor/PhotoSourceDialogFragment.java
+++ b/src/com/android/contacts/editor/PhotoSourceDialogFragment.java
@@ -19,6 +19,7 @@
import com.android.contacts.R;
import com.android.contacts.editor.PhotoActionPopup.ChoiceListItem;
+import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
@@ -44,14 +45,17 @@
void onPickFromGalleryChosen();
}
- public static void show(CompactContactEditorFragment fragment, int photoMode) {
+ public static void show(Activity activity, int photoMode) {
+ if (!(activity instanceof Listener)) {
+ throw new IllegalArgumentException(
+ "Activity must implement " + Listener.class.getName());
+ }
final Bundle args = new Bundle();
args.putInt(ARG_PHOTO_MODE, photoMode);
PhotoSourceDialogFragment dialog = new PhotoSourceDialogFragment();
- dialog.setTargetFragment(fragment, 0);
dialog.setArguments(args);
- dialog.show(fragment.getFragmentManager(), "photoSource");
+ dialog.show(activity.getFragmentManager(), "photoSource");
}
@Override
@@ -69,7 +73,7 @@
final OnClickListener clickListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int which) {
- final Listener listener = (Listener) getTargetFragment();
+ final Listener listener = (Listener) getActivity();
final ChoiceListItem choice = choices.get(which);
switch (choice.getId()) {
case ChoiceListItem.ID_REMOVE: