Messaging: Vendor chips, colorpicker, photoviewer, and vcard in-tree

Clone no longer depends on LineageOS gitlinks; the libraries used by the app live in this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-29 06:19:53 +09:00
co-authored by Cursor
parent a6b23f0f3c
commit 49789fc4a5
239 changed files with 35551 additions and 4 deletions
@@ -0,0 +1,259 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.ex.photo.adapters;
import android.content.Context;
import android.database.Cursor;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import com.android.ex.photo.provider.PhotoContract;
import java.util.HashMap;
/**
* Page adapter for use with an BaseCursorLoader. Unlike other cursor adapters, this has no
* observers for automatic refresh. Instead, it depends upon external mechanisms to provide
* the update signal.
*/
public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
private static final String TAG = "BaseCursorPagerAdapter";
protected Context mContext;
protected Cursor mCursor;
protected int mRowIDColumn;
/** Mapping of row ID to cursor position */
protected SparseIntArray mItemPosition;
/** Mapping of instantiated object to row ID */
protected final HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
/**
* Constructor that always enables auto-requery.
*
* @param c The cursor from which to get the data.
* @param context The context
*/
public BaseCursorPagerAdapter(Context context, FragmentManager fm, Cursor c) {
super(fm);
init(context, c);
}
/**
* Makes a fragment for the data pointed to by the cursor
*
* @param context Interface to application's global information
* @param cursor The cursor from which to get the data. The cursor is already
* moved to the correct position.
* @return the newly created fragment.
*/
public abstract Fragment getItem(Context context, Cursor cursor, int position);
// TODO: This shouldn't just return null - maybe it needs to wait for a cursor to be supplied?
// See b/7103023
@Override
public Fragment getItem(int position) {
if (mCursor != null && moveCursorTo(position)) {
return getItem(mContext, mCursor, position);
}
return null;
}
@Override
public int getCount() {
if (mCursor != null) {
return mCursor.getCount();
} else {
return 0;
}
}
@Override
public Object instantiateItem(View container, int position) {
if (mCursor == null) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
final Integer rowId;
if (moveCursorTo(position)) {
rowId = mCursor.getString(mRowIDColumn).hashCode();
} else {
rowId = null;
}
// Create the fragment and bind cursor data
final Object obj = super.instantiateItem(container, position);
if (obj != null) {
mObjectRowMap.put(obj, rowId);
}
return obj;
}
@Override
public void destroyItem(View container, int position, Object object) {
mObjectRowMap.remove(object);
super.destroyItem(container, position, object);
}
@Override
public int getItemPosition(Object object) {
final Integer rowId = mObjectRowMap.get(object);
if (rowId == null || mItemPosition == null) {
return POSITION_NONE;
}
final int position = mItemPosition.get(rowId, POSITION_NONE);
return position;
}
/**
* @return true if data is valid
*/
public boolean isDataValid() {
return mCursor != null;
}
/**
* Returns the cursor.
*/
public Cursor getCursor() {
return mCursor;
}
/**
* Returns the data item associated with the specified position in the data set.
*/
public Object getDataItem(int position) {
if (mCursor != null && moveCursorTo(position)) {
return mCursor;
} else {
return null;
}
}
/**
* Returns the row id associated with the specified position in the list.
*/
public long getItemId(int position) {
if (mCursor != null && moveCursorTo(position)) {
return mCursor.getString(mRowIDColumn).hashCode();
} else {
return 0;
}
}
/**
* Swap in a new Cursor, returning the old Cursor.
*
* @param newCursor The new cursor to be used.
* @return Returns the previously set Cursor, or null if there was not one.
* If the given new Cursor is the same instance is the previously set
* Cursor, null is also returned.
*/
public Cursor swapCursor(Cursor newCursor) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "swapCursor old=" + (mCursor == null ? -1 : mCursor.getCount()) +
"; new=" + (newCursor == null ? -1 : newCursor.getCount()));
}
if (newCursor == mCursor) {
return null;
}
Cursor oldCursor = mCursor;
mCursor = newCursor;
if (newCursor != null) {
mRowIDColumn = newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
} else {
mRowIDColumn = -1;
}
setItemPosition();
notifyDataSetChanged(); // notify the observers about the new cursor
return oldCursor;
}
/**
* Converts the cursor into a CharSequence. Subclasses should override this
* method to convert their results. The default implementation returns an
* empty String for null values or the default String representation of
* the value.
*
* @param cursor the cursor to convert to a CharSequence
* @return a CharSequence representing the value
*/
public CharSequence convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
@Override
protected String makeFragmentName(int viewId, int index) {
if (moveCursorTo(index)) {
return "android:pager:" + viewId + ":" + mCursor.getString(mRowIDColumn).hashCode();
} else {
return super.makeFragmentName(viewId, index);
}
}
/**
* Moves the cursor to the given position
*
* @return {@code true} if the cursor's position was set. Otherwise, {@code false}.
*/
private boolean moveCursorTo(int position) {
if (mCursor != null && !mCursor.isClosed()) {
return mCursor.moveToPosition(position);
}
return false;
}
/**
* Initialize the adapter.
*/
private void init(Context context, Cursor c) {
boolean cursorPresent = c != null;
mCursor = c;
mContext = context;
mRowIDColumn = cursorPresent
? mCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI) : -1;
}
/**
* Sets the {@link #mItemPosition} instance variable with the current mapping of
* row id to cursor position.
*/
private void setItemPosition() {
if (mCursor == null || mCursor.isClosed()) {
mItemPosition = null;
return;
}
SparseIntArray itemPosition = new SparseIntArray(mCursor.getCount());
mCursor.moveToPosition(-1);
while (mCursor.moveToNext()) {
final int rowId = mCursor.getString(mRowIDColumn).hashCode();
final int position = mCursor.getPosition();
itemPosition.append(rowId, position);
}
mItemPosition = itemPosition;
}
}
@@ -0,0 +1,191 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.ex.photo.adapters;
import android.os.Parcelable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.collection.LruCache;
import androidx.viewpager.widget.PagerAdapter;
import android.util.Log;
import android.view.View;
/**
* NOTE: This is a direct copy of {@link androidx.fragment.app.FragmentPagerAdapter}
* with four very important modifications.
* <p>
* <ol>
* <li>The method {@link #makeFragmentName(int, int)} is declared "protected"
* in our class. We need to be able to re-define the fragment's name according to data
* only available to sub-classes.</li>
* <li>The method {@link #isViewFromObject(View, Object)} has been reimplemented to search
* the entire view hierarchy for the given view.</li>
* <li>In method {@link #destroyItem(View, int, Object)}, the fragment is detached and
* added to a cache. If the fragment is evicted from the cache, it will be deleted.
* An album may contain thousands of photos and we want to avoid having thousands of
* fragments.</li>
* </ol>
*/
public abstract class BaseFragmentPagerAdapter extends PagerAdapter {
/** The default size of {@link #mFragmentCache} */
private static final int DEFAULT_CACHE_SIZE = 5;
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;
/** A cache to store detached fragments before they are removed */
private LruCache<String, Fragment> mFragmentCache = new FragmentCache(DEFAULT_CACHE_SIZE);
public BaseFragmentPagerAdapter(androidx.fragment.app.FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
@Override
public void startUpdate(View container) {
}
@Override
public Object instantiateItem(View container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), position);
// Remove item from the cache
mFragmentCache.remove(name);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + position + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if(fragment == null) {
if (DEBUG) Log.e(TAG, "NPE workaround for getItem(). See b/7103023");
return null;
}
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), position));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
}
return fragment;
}
@Override
public void destroyItem(View container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
Fragment fragment = (Fragment) object;
String name = fragment.getTag();
if (name == null) {
// We prefer to get the name directly from the fragment, but, if the fragment is
// detached before the add transaction is committed, this could be 'null'. In
// that case, generate a name so we can still cache the fragment.
name = makeFragmentName(container.getId(), position);
}
mFragmentCache.put(name, fragment);
mCurTransaction.detach(fragment);
}
@Override
public void setPrimaryItem(View container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(View container) {
if (mCurTransaction != null && !mFragmentManager.isDestroyed()) {
mCurTransaction.commitAllowingStateLoss();
mCurTransaction = null;
mFragmentManager.executePendingTransactions();
}
}
@Override
public boolean isViewFromObject(View view, Object object) {
// Ascend the tree to determine if the view is a child of the fragment
View root = ((Fragment) object).getView();
for (Object v = view; v instanceof View; v = ((View) v).getParent()) {
if (v == root) {
return true;
}
}
return false;
}
@Override
public Parcelable saveState() {
return null;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}
/** Creates a name for the fragment */
protected String makeFragmentName(int viewId, int index) {
return "android:switcher:" + viewId + ":" + index;
}
/**
* A cache of detached fragments.
*/
private class FragmentCache extends LruCache<String, Fragment> {
public FragmentCache(int size) {
super(size);
}
@Override
protected void entryRemoved(boolean evicted, String key,
Fragment oldValue, Fragment newValue) {
// remove the fragment if it's evicted OR it's replaced by a new fragment
if (evicted || (newValue != null && oldValue != newValue)) {
mCurTransaction.remove(oldValue);
}
}
}
}
@@ -0,0 +1,134 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.ex.photo.adapters;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import androidx.fragment.app.Fragment;
import androidx.collection.SimpleArrayMap;
import com.android.ex.photo.Intents;
import com.android.ex.photo.Intents.PhotoViewIntentBuilder;
import com.android.ex.photo.fragments.PhotoViewFragment;
import com.android.ex.photo.provider.PhotoContract.PhotoViewColumns;
import com.android.ex.photo.provider.PhotoContract.PhotoQuery;
/**
* Pager adapter for the photo view
*/
public class PhotoPagerAdapter extends BaseCursorPagerAdapter {
protected SimpleArrayMap<String, Integer> mColumnIndices =
new SimpleArrayMap<String, Integer>(PhotoQuery.PROJECTION.length);
protected final float mMaxScale;
protected boolean mDisplayThumbsFullScreen;
public PhotoPagerAdapter(
Context context, androidx.fragment.app.FragmentManager fm, Cursor c,
float maxScale, boolean thumbsFullScreen) {
super(context, fm, c);
mMaxScale = maxScale;
mDisplayThumbsFullScreen = thumbsFullScreen;
}
@Override
public Fragment getItem(Context context, Cursor cursor, int position) {
final String photoUri = getPhotoUri(cursor);
final String thumbnailUri = getThumbnailUri(cursor);
final String contentDescription = getPhotoName(cursor);
boolean loading = shouldShowLoadingIndicator(cursor);
boolean onlyShowSpinner = false;
if(photoUri == null && loading) {
onlyShowSpinner = true;
}
// create new PhotoViewFragment
final PhotoViewIntentBuilder builder =
Intents.newPhotoViewFragmentIntentBuilder(mContext, getPhotoViewFragmentClass());
builder
.setResolvedPhotoUri(photoUri)
.setThumbnailUri(thumbnailUri)
.setContentDescription(contentDescription)
.setDisplayThumbsFullScreen(mDisplayThumbsFullScreen)
.setMaxInitialScale(mMaxScale);
return createPhotoViewFragment(builder.build(), position, onlyShowSpinner);
}
protected Class<? extends PhotoViewFragment> getPhotoViewFragmentClass() {
return PhotoViewFragment.class;
}
protected PhotoViewFragment createPhotoViewFragment(
Intent intent, int position, boolean onlyShowSpinner) {
return PhotoViewFragment.newInstance(intent, position, onlyShowSpinner);
}
@Override
public Cursor swapCursor(Cursor newCursor) {
mColumnIndices.clear();
if (newCursor != null) {
for(String column : PhotoQuery.PROJECTION) {
mColumnIndices.put(column, newCursor.getColumnIndexOrThrow(column));
}
for(String column : PhotoQuery.OPTIONAL_COLUMNS) {
int index = newCursor.getColumnIndex(column);
if (index != -1) {
mColumnIndices.put(column, index);
}
}
}
return super.swapCursor(newCursor);
}
public String getPhotoUri(Cursor cursor) {
return getString(cursor, PhotoViewColumns.CONTENT_URI);
}
public String getThumbnailUri(Cursor cursor) {
return getString(cursor, PhotoViewColumns.THUMBNAIL_URI);
}
public String getContentType(Cursor cursor) {
return getString(cursor, PhotoViewColumns.CONTENT_TYPE);
}
public String getPhotoName(Cursor cursor) {
return getString(cursor, PhotoViewColumns.NAME);
}
public boolean shouldShowLoadingIndicator(Cursor cursor) {
String value = getString(cursor, PhotoViewColumns.LOADING_INDICATOR);
if (value == null) {
return false;
} else {
return Boolean.parseBoolean(value);
}
}
private String getString(Cursor cursor, String column) {
if (mColumnIndices.containsKey(column)) {
return cursor.getString(mColumnIndices.get(column));
} else {
return null;
}
}
}