diff options
author | Matt Garnes <matt@cyngn.com> | 2015-03-17 13:43:50 -0700 |
---|---|---|
committer | Matt Garnes <matt@cyngn.com> | 2015-03-17 13:43:50 -0700 |
commit | d8284f229dfa8d0e2aef3cac0828362e1e6632e1 (patch) | |
tree | 57b913502af687d5a27c3ce3a82b964802b5112c /src | |
parent | 4c3b2364453fb21dc3f3f8a70e1bcc64e500d83d (diff) | |
parent | 06e3e072e0b70b01dd1d4e2d859d70832d16172c (diff) | |
download | android_packages_apps_Trebuchet-d8284f229dfa8d0e2aef3cac0828362e1e6632e1.tar.gz android_packages_apps_Trebuchet-d8284f229dfa8d0e2aef3cac0828362e1e6632e1.tar.bz2 android_packages_apps_Trebuchet-d8284f229dfa8d0e2aef3cac0828362e1e6632e1.zip |
Merge branch 'github/staging/cm-12.1' into caf/cm-12.1.
Conflicts:
WallpaperPicker/res/values-nodpi/wallpapers.xml
res/layout/settings_pane.xml
res/values-zh-rCN/cm_strings.xml
res/values/preferences_defaults.xml
src/com/android/launcher3/DynamicGrid.java
Change-Id: I22eb2e38e8802985e0e0a8cb3e68d73189c2c1a4
Diffstat (limited to 'src')
18 files changed, 2603 insertions, 47 deletions
diff --git a/src/com/android/launcher3/AppDrawerIconView.java b/src/com/android/launcher3/AppDrawerIconView.java new file mode 100644 index 000000000..d8564b6ed --- /dev/null +++ b/src/com/android/launcher3/AppDrawerIconView.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.launcher3; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * AppDrawerIconView - represents icons in the vertical app drawer. + * Found to be more performant than the BubbleTextView used in the + * legacy app drawer. + */ +public class AppDrawerIconView extends LinearLayout { + + TextView mLabel; + ImageView mIcon; + + public AppDrawerIconView(Context context) { + super(context); + } + + public AppDrawerIconView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AppDrawerIconView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mLabel = (TextView) findViewById(R.id.label); + mIcon = (ImageView) findViewById(R.id.image); + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + mLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); + mLabel.setShadowLayer(BubbleTextView.SHADOW_LARGE_RADIUS, 0.0f, + BubbleTextView.SHADOW_Y_OFFSET, BubbleTextView.SHADOW_LARGE_COLOUR); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + setAlpha(PagedViewIcon.PRESS_ALPHA); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + setAlpha(1f); + break; + } + return super.onTouchEvent(event); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/AppDrawerListAdapter.java b/src/com/android/launcher3/AppDrawerListAdapter.java new file mode 100644 index 000000000..a16937308 --- /dev/null +++ b/src/com/android/launcher3/AppDrawerListAdapter.java @@ -0,0 +1,566 @@ +/* + * Copyright (C) 2015 The CyanogenMod 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.launcher3; + +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.support.v7.widget.RecyclerView; +import android.widget.LinearLayout; +import android.widget.SectionIndexer; +import com.android.launcher3.locale.LocaleSetManager; +import com.android.launcher3.locale.LocaleUtils; +import com.android.launcher3.settings.SettingsProvider; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; + +/** + * AppDrawerListAdapter - list adapter for the vertical app drawer + */ +public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdapter.ViewHolder> + implements View.OnLongClickListener, DragSource, SectionIndexer { + + private static final String NUMERIC_OR_SPECIAL_HEADER = "#"; + + private ArrayList<AppItemIndexedInfo> mHeaderList; + private LayoutInflater mLayoutInflater; + + private Launcher mLauncher; + private DeviceProfile mDeviceProfile; + private LinkedHashMap<String, Integer> mSectionHeaders; + private LinearLayout.LayoutParams mIconParams; + private Rect mIconRect; + private LocaleSetManager mLocaleSetManager; + + private ArrayList<ComponentName> mProtectedApps; + + private boolean mHideIconLabels; + + public enum DrawerType { + Drawer(0), + Pager(1); + + private final int mValue; + private DrawerType(int value) { + mValue = value; + } + + public int getValue() { + return mValue; + } + + public static DrawerType getModeForValue(int value) { + switch (value) { + case 1: + return Pager; + default : + return Drawer; + } + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public AutoFitTextView mTextView; + public ViewGroup mLayout; + public ViewHolder(View itemView) { + super(itemView); + mTextView = (AutoFitTextView) itemView.findViewById(R.id.drawer_item_title); + mLayout = (ViewGroup) itemView.findViewById(R.id.drawer_item_flow); + } + } + + public AppDrawerListAdapter(Launcher launcher) { + mLauncher = launcher; + mHeaderList = new ArrayList<AppItemIndexedInfo>(); + mLayoutInflater = LayoutInflater.from(launcher); + + mLocaleSetManager = new LocaleSetManager(mLauncher); + mLocaleSetManager.updateLocaleSet(mLocaleSetManager.getSystemLocaleSet()); + initParams(); + + updateProtectedAppsList(mLauncher); + } + + private void initParams() { + mDeviceProfile = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + + int width = mDeviceProfile.cellWidthPx + 2 * mDeviceProfile.edgeMarginPx; + mIconParams = new + LinearLayout.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT); + mIconRect = new Rect(0, 0, mDeviceProfile.allAppsIconSizePx, + mDeviceProfile.allAppsIconSizePx); + + mHideIconLabels = SettingsProvider.getBoolean(mLauncher, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default); + } + + /** + * Create and populate mHeaderList (buckets for app sorting) + * @param info + */ + public void populateByCharacter(ArrayList<AppInfo> info) { + if (info == null || info.size() <= 0) { + Collections.sort(mHeaderList); + return; + } + + // Create a clone of AppInfo ArrayList to preserve data + ArrayList<AppInfo> tempInfo = (ArrayList<AppInfo>) info.clone(); + + ArrayList<AppInfo> appInfos = new ArrayList<AppInfo>(); + + // get next app + AppInfo app = tempInfo.get(0); + + // get starting character + LocaleUtils localeUtils = LocaleUtils.getInstance(); + int bucketIndex = localeUtils.getBucketIndex(app.title.toString()); + String startString + = localeUtils.getBucketLabel(bucketIndex); + if (TextUtils.isEmpty(startString)) { + startString = NUMERIC_OR_SPECIAL_HEADER; + bucketIndex = localeUtils.getBucketIndex(startString); + } + + // now iterate through + for (AppInfo info1 : tempInfo) { + int newBucketIndex = localeUtils.getBucketIndex(info1.title.toString()); + + String newChar + = localeUtils.getBucketLabel(newBucketIndex); + if (TextUtils.isEmpty(newChar)) { + newChar = NUMERIC_OR_SPECIAL_HEADER; + } + // if same character + if (newChar.equals(startString)) { + // add it + appInfos.add(info1); + } + } + + Collections.sort(appInfos, LauncherModel.getAppNameComparator()); + + for (int i = 0; i < appInfos.size(); i += mDeviceProfile.numColumnsBase) { + int endIndex = (int) Math.min(i + mDeviceProfile.numColumnsBase, appInfos.size()); + ArrayList<AppInfo> subList = new ArrayList<AppInfo>(appInfos.subList(i, endIndex)); + AppItemIndexedInfo indexInfo; + indexInfo = new AppItemIndexedInfo(startString, bucketIndex, subList, i != 0); + mHeaderList.add(indexInfo); + } + + for (AppInfo remove : appInfos) { + // remove from mApps + tempInfo.remove(remove); + } + populateByCharacter(tempInfo); + } + + public void setApps(ArrayList<AppInfo> list) { + if (!LauncherAppState.isDisableAllApps()) { + initParams(); + + filterProtectedApps(list); + + mHeaderList.clear(); + populateByCharacter(list); + populateSectionHeaders(); + mLauncher.updateScrubber(); + this.notifyDataSetChanged(); + } + } + + private void populateSectionHeaders() { + if (mSectionHeaders == null || mSectionHeaders.size() != mHeaderList.size()) { + mSectionHeaders = new LinkedHashMap<String, Integer>(); + } + int count = 0; + for (int i = 0; i < mHeaderList.size(); i++) { + AppItemIndexedInfo info = mHeaderList.get(i); + if (!mHeaderList.get(i).isChild) { + mSectionHeaders.put(String.valueOf(mHeaderList.get(i).mStartString), count); + } + if (info.mInfo.size() < mDeviceProfile.numColumnsBase) { + count++; + } else { + count += info.mInfo.size() / mDeviceProfile.numColumnsBase; + } + } + } + + public void reset() { + ArrayList<AppInfo> infos = getAllApps(); + + mLauncher.mAppDrawer.getLayoutManager().removeAllViews(); + setApps(infos); + } + + private ArrayList<AppInfo> getAllApps() { + ArrayList<AppInfo> indexedInfos = new ArrayList<AppInfo>(); + + for (int j = 0; j < mHeaderList.size(); ++j) { + AppItemIndexedInfo indexedInfo = mHeaderList.get(j); + for (AppInfo info : indexedInfo.mInfo) { + indexedInfos.add(info); + } + } + return indexedInfos; + } + + public void updateApps(ArrayList<AppInfo> list) { + // We remove and re-add the updated applications list because it's properties may have + // changed (ie. the title), and this will ensure that the items will be in their proper + // place in the list. + if (!LauncherAppState.isDisableAllApps()) { + removeAppsWithoutInvalidate(list); + addAppsWithoutInvalidate(list); + reset(); + } + } + + + public void addApps(ArrayList<AppInfo> list) { + if (!LauncherAppState.isDisableAllApps()) { + addAppsWithoutInvalidate(list); + reset(); + } + } + + private void addAppsWithoutInvalidate(ArrayList<AppInfo> list) { + // We add it in place, in alphabetical order + LocaleUtils localeUtils = LocaleUtils.getInstance(); + + int count = list.size(); + for (int i = 0; i < count; ++i) { + AppInfo info = list.get(i); + boolean found = false; + AppItemIndexedInfo lastInfoForSection = null; + int bucketIndex = localeUtils.getBucketIndex(info.title.toString()); + String start = localeUtils.getBucketLabel(bucketIndex); + if (TextUtils.isEmpty(start)) { + start = NUMERIC_OR_SPECIAL_HEADER; + bucketIndex = localeUtils.getBucketIndex(start); + } + for (int j = 0; j < mHeaderList.size(); ++j) { + AppItemIndexedInfo indexedInfo = mHeaderList.get(j); + if (start.equals(indexedInfo.mStartString)) { + Collections.sort(indexedInfo.mInfo, LauncherModel.getAppNameComparator()); + int index = + Collections.binarySearch(indexedInfo.mInfo, + info, LauncherModel.getAppNameComparator()); + if (index >= 0) { + found = true; + break; + } else { + lastInfoForSection = indexedInfo; + } + } + } + if (!found) { + if (lastInfoForSection != null) { + lastInfoForSection.mInfo.add(info); + } else { + // we need to create a new section + ArrayList<AppInfo> newInfos = new ArrayList<AppInfo>(); + newInfos.add(info); + AppItemIndexedInfo newInfo = + new AppItemIndexedInfo(start, bucketIndex, newInfos, false); + mHeaderList.add(newInfo); + Collections.sort(mHeaderList); + } + } + } + } + + public void removeApps(ArrayList<AppInfo> appInfos) { + if (!LauncherAppState.isDisableAllApps()) { + removeAppsWithoutInvalidate(appInfos); + //recreate everything + reset(); + } + } + + private void removeAppsWithoutInvalidate(ArrayList<AppInfo> list) { + // loop through all the apps and remove apps that have the same component + int length = list.size(); + for (int i = 0; i < length; ++i) { + AppInfo info = list.get(i); + for (int j = 0; j < mHeaderList.size(); ++j) { + AppItemIndexedInfo indexedInfo = mHeaderList.get(j); + ArrayList<AppInfo> clonedIndexedInfoApps = + (ArrayList<AppInfo>) indexedInfo.mInfo.clone(); + int index = + findAppByComponent(clonedIndexedInfoApps, info); + if (index > -1) { + indexedInfo.mInfo.remove(info); + } + } + } + } + + private int findAppByComponent(List<AppInfo> list, AppInfo item) { + ComponentName removeComponent = item.intent.getComponent(); + int length = list.size(); + for (int i = 0; i < length; ++i) { + AppInfo info = list.get(i); + if (info.intent.getComponent().equals(removeComponent)) { + return i; + } + } + return -1; + } + + /* + * AllAppsView implementation + */ + public void setup(Launcher launcher) { + mLauncher = launcher; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()). + inflate(R.layout.app_drawer_item, parent, false); + ViewHolder holder = new ViewHolder(v); + holder.mTextView.setPadding(0, 0, 0, mDeviceProfile.iconTextSizePx + 10); + for (int i = 0; i < mDeviceProfile.numColumnsBase; i++) { + AppDrawerIconView icon = (AppDrawerIconView) mLayoutInflater.inflate( + R.layout.drawer_icon, holder.mLayout, false); + icon.setOnClickListener(mLauncher); + icon.setOnLongClickListener(this); + holder.mLayout.addView(icon); + } + return holder; + } + + @Override + public int getItemCount() { + return mHeaderList.size(); + } + + public AppItemIndexedInfo getItemAt(int position) { + if (position < mHeaderList.size()) + return mHeaderList.get(position); + return null; + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AppItemIndexedInfo indexedInfo = mHeaderList.get(position); + holder.mTextView.setVisibility(indexedInfo.isChild ? View.INVISIBLE : View.VISIBLE); + if (!indexedInfo.isChild) { + if (indexedInfo.mStartString.equals(NUMERIC_OR_SPECIAL_HEADER)) { + holder.mTextView.setText(NUMERIC_OR_SPECIAL_HEADER); + } else { + holder.mTextView.setText(String.valueOf(indexedInfo.mStartString)); + } + } + final int size = indexedInfo.mInfo.size(); + for (int i = 0; i < holder.mLayout.getChildCount(); i++) { + AppDrawerIconView icon = (AppDrawerIconView) holder.mLayout.getChildAt(i); + icon.setLayoutParams(mIconParams); + if (i >= size) { + icon.setVisibility(View.INVISIBLE); + } else { + icon.setVisibility(View.VISIBLE); + AppInfo info = indexedInfo.mInfo.get(i); + icon.setTag(info); + Drawable d = Utilities.createIconDrawable(info.iconBitmap); + d.setBounds(mIconRect); + icon.mIcon.setImageDrawable(d); + icon.mLabel.setText(info.title); + icon.mLabel.setVisibility(mHideIconLabels ? View.INVISIBLE : View.VISIBLE); + } + } + holder.itemView.setTag(indexedInfo); + } + + @Override + public boolean onLongClick(View v) { + if (v instanceof AppDrawerIconView) { + beginDraggingApplication(v); + mLauncher.enterSpringLoadedDragMode(); + } + return false; + } + + @Override + public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, + boolean success) { + // Return early and wait for onFlingToDeleteCompleted if this was the result of a fling + if (isFlingToDelete) return; + + endDragging(target, false, success); + + // Display an error message if the drag failed due to there not being enough space on the + // target layout we were dropping on. + if (!success) { + boolean showOutOfSpaceMessage = false; + if (target instanceof Workspace) { + int currentScreen = mLauncher.getCurrentWorkspaceScreen(); + Workspace workspace = (Workspace) target; + CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); + ItemInfo itemInfo = (ItemInfo) d.dragInfo; + if (layout != null) { + layout.calculateSpans(itemInfo); + showOutOfSpaceMessage = + !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); + } + } + if (showOutOfSpaceMessage) { + mLauncher.showOutOfSpaceMessage(false); + } + + d.deferDragViewCleanupPostAnimation = false; + } + } + + /** + * Clean up after dragging. + * + * @param target where the item was dragged to (can be null if the item was flung) + */ + private void endDragging(View target, boolean isFlingToDelete, boolean success) { + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && + !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { + // Exit spring loaded mode if we have not successfully dropped or have not handled the + // drop in Workspace + mLauncher.getWorkspace().removeExtraEmptyScreenDelayed(true, new Runnable() { + @Override + public void run() { + mLauncher.exitSpringLoadedDragMode(); + mLauncher.unlockScreenOrientation(false); + } + }, 0, true); + } else { + mLauncher.unlockScreenOrientation(false); + } + } + + @Override + public boolean supportsFlingToDelete() { + return false; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return true; + } + + @Override + public boolean supportsDeleteDropTarget() { + return false; + } + + @Override + public float getIntrinsicIconScaleFactor() { + return (float) mDeviceProfile.allAppsIconSizePx / mDeviceProfile.iconSizePx; + } + + private void beginDraggingApplication(View v) { + mLauncher.getWorkspace().beginDragShared(v, this); + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + } + + public class AppItemIndexedInfo implements Comparable { + private boolean isChild; + private String mStartString; + private int mStringIndex; + private ArrayList<AppInfo> mInfo; + + private AppItemIndexedInfo(String startString, int bucketIndex, ArrayList<AppInfo> info, + boolean isChild) { + this.mStartString = startString; + this.mStringIndex = bucketIndex; + this.mInfo = info; + this.isChild = isChild; + + if (mStartString.equals(NUMERIC_OR_SPECIAL_HEADER)) { + this.mStringIndex = 0; + } + } + + public String getString() { + return mStartString; + } + + @Override + public int compareTo(Object o) { + if (o instanceof AppItemIndexedInfo) { + int otherBucketIndex = ((AppItemIndexedInfo) o).mStringIndex; + return Integer.compare(mStringIndex, otherBucketIndex); + } + return 0; + } + } + + @Override + public Object[] getSections() { + return mSectionHeaders.keySet().toArray(new String[mSectionHeaders.size()]); + } + + @Override + public int getPositionForSection(int sectionIndex) { + return mSectionHeaders.get(getSections()[sectionIndex]); + } + + @Override + public int getSectionForPosition(int position) { + return mSectionHeaders.get(mHeaderList.get(position).mStartString); + } + + private void filterProtectedApps(ArrayList<AppInfo> list) { + updateProtectedAppsList(mLauncher); + + Iterator<AppInfo> iterator = list.iterator(); + while (iterator.hasNext()) { + AppInfo appInfo = iterator.next(); + if (mProtectedApps.contains(appInfo.componentName)) { + iterator.remove(); + } + } + } + + private void updateProtectedAppsList(Context context) { + String protectedComponents = Settings.Secure.getString(context.getContentResolver(), + LauncherModel.SETTINGS_PROTECTED_COMPONENTS); + protectedComponents = protectedComponents == null ? "" : protectedComponents; + String [] flattened = protectedComponents.split("\\|"); + mProtectedApps = new ArrayList<ComponentName>(flattened.length); + for (String flat : flattened) { + ComponentName cmp = ComponentName.unflattenFromString(flat); + if (cmp != null) { + mProtectedApps.add(cmp); + } + } + } +} diff --git a/src/com/android/launcher3/AppDrawerScrubber.java b/src/com/android/launcher3/AppDrawerScrubber.java new file mode 100644 index 000000000..0ace60da8 --- /dev/null +++ b/src/com/android/launcher3/AppDrawerScrubber.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.launcher3; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +public class AppDrawerScrubber extends LinearLayout { + + private final int SCRUBBER_INDICATOR_DISPLAY_DURATION = 200; + private final float SCRUBBER_INDICATOR_DISPLAY_TRANSLATIONY = 20f; + + private AppDrawerListAdapter mAdapter; + private RecyclerView mListView; + private TextView mScrubberIndicator; + private SeekBar mSeekBar; + private String[] mSections; + private LinearLayoutManager mLayoutManager; + + public AppDrawerScrubber(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public AppDrawerScrubber(Context context) { + super(context); + init(context); + } + + public void updateSections() { + mSections = (String[]) mAdapter.getSections(); + mSeekBar.setMax(mSections.length - 1); + } + + public void setSource(RecyclerView listView) { + mListView = listView; + mAdapter = (AppDrawerListAdapter) listView.getAdapter(); + mLayoutManager = (LinearLayoutManager) listView.getLayoutManager(); + } + + public void setScrubberIndicator(TextView scrubberIndicator) { + mScrubberIndicator = scrubberIndicator; + } + + private boolean isReady() { + return mListView != null && + mAdapter != null && + mSections != null; + } + + private void init(Context context) { + LayoutInflater.from(context).inflate(R.layout.scrub_layout, this); + mSeekBar = (SeekBar) findViewById(R.id.scrubber); + + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, final int progress, boolean fromUser) { + if (!isReady()) { + return; + } + resetScrubber(); + + String section = String.valueOf(mSections[progress]); + + if (mScrubberIndicator != null) { + float translateX = (progress * seekBar.getWidth()) / mSections.length; + translateX -= (mScrubberIndicator.getWidth() / 6); // offset for alignment + mScrubberIndicator.setTranslationX(translateX); + mScrubberIndicator.setText(section); + } + + mLayoutManager.smoothScrollToPosition(mListView, null, + mAdapter.getPositionForSection(progress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + resetScrubber(); + if (mScrubberIndicator != null) { + mScrubberIndicator.setAlpha(1f); + mScrubberIndicator.setVisibility(View.VISIBLE); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + resetScrubber(); + if (mScrubberIndicator != null) { + mScrubberIndicator.animate().alpha(0f).translationYBy(20f) + .setDuration(200).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mScrubberIndicator.setVisibility(View.INVISIBLE); + } + }); + } + } + + private void resetScrubber() { + if (mScrubberIndicator != null) { + mScrubberIndicator.animate().cancel(); + mScrubberIndicator.setTranslationY(0f); + } + } + }); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/AutoFitTextView.java b/src/com/android/launcher3/AutoFitTextView.java new file mode 100644 index 000000000..208dd4073 --- /dev/null +++ b/src/com/android/launcher3/AutoFitTextView.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2014 Grantland Chew + * + * 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.launcher3; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.method.TransformationMethod; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.widget.TextView; + +/** + * A TextView that resizes it's text to be no larger than the width of the view. + * + * @author Grantland Chew <grantlandchew@gmail.com> + */ +public class AutoFitTextView extends TextView { + + private static final String TAG = "AutoFitTextView"; + private static final boolean SPEW = false; + + // Minimum size of the text in pixels + private static final int DEFAULT_MIN_TEXT_SIZE = 8; //sp + // How precise we want to be when reaching the target textWidth size + private static final float PRECISION = 0.5f; + + // Attributes + private boolean mSizeToFit; + private int mMaxLines; + private float mMinTextSize; + private float mMaxTextSize; + private float mPrecision; + private TextPaint mPaint; + + public AutoFitTextView(Context context) { + super(context); + init(context, null, 0); + } + + public AutoFitTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public AutoFitTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity; + boolean sizeToFit = true; + int minTextSize = (int) scaledDensity * DEFAULT_MIN_TEXT_SIZE; + float precision = PRECISION; + + if (attrs != null) { + TypedArray ta = context.obtainStyledAttributes( + attrs, + R.styleable.AutofitTextView, + defStyle, + 0); + sizeToFit = ta.getBoolean(R.styleable.AutofitTextView_sizeToFit, sizeToFit); + minTextSize = ta.getDimensionPixelSize(R.styleable.AutofitTextView_minTextSize, + minTextSize); + precision = ta.getFloat(R.styleable.AutofitTextView_precision, precision); + ta.recycle(); + } + + mPaint = new TextPaint(); + setSizeToFit(sizeToFit); + setRawTextSize(super.getTextSize()); + setRawMinTextSize(minTextSize); + setPrecision(precision); + } + + // Getters and Setters + + /** + * @return whether or not the text will be automatically resized to fit its constraints. + */ + public boolean isSizeToFit() { + return mSizeToFit; + } + + /** + * Sets the property of this field (singleLine, to automatically resize the text to fit its constraints. + */ + public void setSizeToFit() { + setSizeToFit(true); + } + + /** + * If true, the text will automatically be resized to fit its constraints; if false, it will + * act like a normal TextView. + * + * @param sizeToFit + */ + public void setSizeToFit(boolean sizeToFit) { + mSizeToFit = sizeToFit; + refitText(); + } + + /** + * {@inheritDoc} + */ + @Override + public float getTextSize() { + return mMaxTextSize; + } + + /** + * {@inheritDoc} + */ + @Override + public void setTextSize(int unit, float size) { + Context context = getContext(); + Resources r = Resources.getSystem(); + + if (context != null) { + r = context.getResources(); + } + + setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics())); + } + + private void setRawTextSize(float size) { + if (size != mMaxTextSize) { + mMaxTextSize = size; + refitText(); + } + } + + /** + * @return the minimum size (in pixels) of the text size in this AutofitTextView + */ + public float getMinTextSize() { + return mMinTextSize; + } + + /** + * Set the minimum text size to a given unit and value. See TypedValue for the possible + * dimension units. + * + * @param unit The desired dimension unit. + * @param minSize The desired size in the given units. + * + * @attr ref me.grantland.R.styleable#AutofitTextView_minTextSize + */ + public void setMinTextSize(int unit, float minSize) { + Context context = getContext(); + Resources r = Resources.getSystem(); + + if (context != null) { + r = context.getResources(); + } + + setRawMinTextSize(TypedValue.applyDimension(unit, minSize, r.getDisplayMetrics())); + } + + /** + * Set the minimum text size to the given value, interpreted as "scaled pixel" units. This size + * is adjusted based on the current density and user font size preference. + * + * @param minSize The scaled pixel size. + * + * @attr ref me.grantland.R.styleable#AutofitTextView_minTextSize + */ + public void setMinTextSize(int minSize) { + setMinTextSize(TypedValue.COMPLEX_UNIT_SP, minSize); + } + + private void setRawMinTextSize(float minSize) { + if (minSize != mMinTextSize) { + mMinTextSize = minSize; + refitText(); + } + } + + /** + * @return the amount of precision used to calculate the correct text size to fit within it's + * bounds. + */ + public float getPrecision() { + return mPrecision; + } + + /** + * Set the amount of precision used to calculate the correct text size to fit within it's + * bounds. Lower precision is more precise and takes more time. + * + * @param precision The amount of precision. + */ + public void setPrecision(float precision) { + if (precision != mPrecision) { + mPrecision = precision; + refitText(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setLines(int lines) { + super.setLines(lines); + mMaxLines = lines; + refitText(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getMaxLines() { + return mMaxLines; + } + + /** + * {@inheritDoc} + */ + @Override + public void setMaxLines(int maxLines) { + super.setMaxLines(maxLines); + if (maxLines != mMaxLines) { + mMaxLines = maxLines; + refitText(); + } + } + + /** + * Re size the font so the specified text fits in the text box assuming the text box is the + * specified width. + */ + private void refitText() { + if (!mSizeToFit) { + return; + } + + if (mMaxLines <= 0) { + // Don't auto-size since there's no limit on lines. + return; + } + + CharSequence text = getText(); + TransformationMethod method = getTransformationMethod(); + if (method != null) { + text = method.getTransformation(text, this); + } + int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + if (targetWidth > 0) { + Context context = getContext(); + Resources r = Resources.getSystem(); + DisplayMetrics displayMetrics; + + float size = mMaxTextSize; + float high = size; + float low = 0; + + if (context != null) { + r = context.getResources(); + } + displayMetrics = r.getDisplayMetrics(); + + mPaint.set(getPaint()); + mPaint.setTextSize(size); + + if ((mMaxLines == 1 && mPaint.measureText(text, 0, text.length()) > targetWidth) + || getLineCount(text, mPaint, size, targetWidth, displayMetrics) > mMaxLines) { + size = getTextSize(text, mPaint, targetWidth, mMaxLines, low, high, mPrecision, + displayMetrics); + } + + if (size < mMinTextSize) { + size = mMinTextSize; + } + + super.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); + } + } + + /** + * Recursive binary search to find the best size for the text + */ + private static float getTextSize(CharSequence text, TextPaint paint, + float targetWidth, int maxLines, + float low, float high, float precision, + DisplayMetrics displayMetrics) { + float mid = (low + high) / 2.0f; + int lineCount = 1; + StaticLayout layout = null; + + paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mid, + displayMetrics)); + + if (maxLines != 1) { + layout = new StaticLayout(text, paint, (int)targetWidth, Layout.Alignment.ALIGN_NORMAL, + 1.0f, 0.0f, true); + lineCount = layout.getLineCount(); + } + + if (SPEW) Log.d(TAG, "low=" + low + " high=" + high + " mid=" + mid + + " target=" + targetWidth + " maxLines=" + maxLines + " lineCount=" + lineCount); + + if (lineCount > maxLines) { + return getTextSize(text, paint, targetWidth, maxLines, low, mid, precision, + displayMetrics); + } + else if (lineCount < maxLines) { + return getTextSize(text, paint, targetWidth, maxLines, mid, high, precision, + displayMetrics); + } + else { + float maxLineWidth = 0; + if (maxLines == 1) { + maxLineWidth = paint.measureText(text, 0, text.length()); + } else { + for (int i = 0; i < lineCount; i++) { + if (layout.getLineWidth(i) > maxLineWidth) { + maxLineWidth = layout.getLineWidth(i); + } + } + } + + if ((high - low) < precision) { + return low; + } else if (maxLineWidth > targetWidth) { + return getTextSize(text, paint, targetWidth, maxLines, low, mid, precision, + displayMetrics); + } else if (maxLineWidth < targetWidth) { + return getTextSize(text, paint, targetWidth, maxLines, mid, high, precision, + displayMetrics); + } else { + return mid; + } + } + } + + private static int getLineCount(CharSequence text, TextPaint paint, float size, float width, + DisplayMetrics displayMetrics) { + paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, size, + displayMetrics)); + StaticLayout layout = new StaticLayout(text, paint, (int)width, + Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); + return layout.getLineCount(); + } + + @Override + protected void onTextChanged(final CharSequence text, final int start, + final int lengthBefore, final int lengthAfter) { + super.onTextChanged(text, start, lengthBefore, lengthAfter); + refitText(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (w != oldw) { + refitText(); + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 5bc17b674..c7e85575a 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -42,11 +42,11 @@ public class BubbleTextView extends TextView { private static SparseArray<Theme> sPreloaderThemes = new SparseArray<>(2); - private static final float SHADOW_LARGE_RADIUS = 4.0f; - private static final float SHADOW_SMALL_RADIUS = 1.75f; - private static final float SHADOW_Y_OFFSET = 2.0f; - private static final int SHADOW_LARGE_COLOUR = 0xDD000000; - private static final int SHADOW_SMALL_COLOUR = 0xCC000000; + public static final float SHADOW_LARGE_RADIUS = 4.0f; + public static final float SHADOW_SMALL_RADIUS = 1.75f; + public static final float SHADOW_Y_OFFSET = 2.0f; + public static final int SHADOW_LARGE_COLOUR = 0xDD000000; + public static final int SHADOW_SMALL_COLOUR = 0xCC000000; static final float PADDING_V = 3.0f; private HolographicOutlineHelper mOutlineHelper; diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index 691b09558..328c31173 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -135,6 +135,10 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang lp.bottomMargin += insets.bottom - mInsets.bottom; layout.setLayoutParams(lp); continue; + } else if (child.getId() == R.id.app_drawer_container) { + setAppDrawerInsets(child, insets); + + continue; } setInsets(child, insets, mInsets); if (child.getId() == R.id.search_drop_target_bar) { @@ -145,6 +149,23 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang return true; // I'll take it from here } + private void setAppDrawerInsets(View child, Rect insets) { + // List view + View view = child.findViewById(R.id.app_drawer_recyclerview); + FrameLayout.LayoutParams lp = + (FrameLayout.LayoutParams) view.getLayoutParams(); + int paddingBottom = view.getPaddingBottom() + insets.bottom - mInsets.bottom; + int paddingTop = view.getPaddingTop() + insets.top - mInsets.top; + view.setLayoutParams(lp); + view.setPadding(view.getPaddingLeft(), paddingTop, view.getPaddingRight(), paddingBottom); + + // Scrubber + view = child.findViewById(R.id.app_drawer_scrubber_container); + LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) view.getLayoutParams(); + llp.bottomMargin += insets.bottom - mInsets.bottom; + view.setLayoutParams(llp); + } + Rect getInsets() { return mInsets; } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 80781c642..15df77fe6 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -70,6 +70,8 @@ import android.os.Message; import android.os.StrictMode; import android.os.SystemClock; import android.speech.RecognizerIntent; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -98,6 +100,7 @@ import android.view.accessibility.AccessibilityEvent; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Advanceable; import android.widget.FrameLayout; @@ -166,6 +169,8 @@ public class Launcher extends Activity private static final int REQUEST_RECONFIGURE_APPWIDGET = 12; public static final int REQUEST_TRANSITION_EFFECTS = 14; + private static final float OVERSHOOT_TENSION = 1.4f; + static final int REQUEST_PICK_ICON = 13; private static final int REQUEST_LOCK_PATTERN = 14; @@ -299,6 +304,9 @@ public class Launcher extends Activity OverviewSettingsPanel mOverviewSettingsPanel; private View mAllAppsButton; + protected RecyclerView mAppDrawer; + private AppDrawerListAdapter mAppDrawerAdapter; + private AppDrawerScrubber mScrubber; protected SearchDropTargetBar mSearchDropTargetBar; private AppsCustomizeTabHost mAppsCustomizeTabHost; @@ -389,6 +397,7 @@ public class Launcher extends Activity // Preferences private boolean mHideIconLabels; + private AppDrawerListAdapter.DrawerType mDrawerType; private Runnable mBuildLayersRunnable = new Runnable() { public void run() { @@ -563,6 +572,10 @@ public class Launcher extends Activity mHideIconLabels = SettingsProvider.getBoolean(this, SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, R.bool.preferences_interface_homescreen_hide_icon_labels_default); + mDrawerType = AppDrawerListAdapter.DrawerType.getModeForValue( + SettingsProvider.getInt(this, + SettingsProvider.SETTINGS_UI_DRAWER_TYPE, + R.integer.preferences_interface_drawer_type_default)); // Determine the dynamic grid properties Point smallestSize = new Point(); @@ -598,6 +611,24 @@ public class Launcher extends Activity protected void populateCustomContentContainer() { } + private void initializeScrubber() { + if (mScrubber == null) { + FrameLayout view = (FrameLayout) findViewById(R.id.app_drawer_container); + mScrubber = (AppDrawerScrubber) view.findViewById(R.id.app_drawer_scrubber); + mScrubber.setSource(mAppDrawer); + mScrubber.setScrubberIndicator((TextView) view.findViewById(R.id.scrubberIndicator)); + } + } + + public void updateScrubber() { + mScrubber.updateSections(); + } + + public void initializeAdapter() { + mAppDrawerAdapter = new AppDrawerListAdapter(this); + mAppDrawerAdapter.notifyDataSetChanged(); + } + /** * Invoked by subclasses to signal a change to the {@link #addCustomContentToLeft} value to * ensure the custom content page is added or removed if necessary. @@ -1239,6 +1270,17 @@ public class Launcher extends Activity return false; } + public void updateDrawerType() { + mDrawerType = AppDrawerListAdapter.DrawerType.getModeForValue( + SettingsProvider.getInt(this, + SettingsProvider.SETTINGS_UI_DRAWER_TYPE, + R.integer.preferences_interface_drawer_type_default)); + } + + public AppDrawerListAdapter.DrawerType getDrawerType() { + return mDrawerType; + } + public void onClickSortModeButton(View v) { final PopupMenu popupMenu = new PopupMenu(this, v); final Menu menu = popupMenu.getMenu(); @@ -1289,7 +1331,18 @@ public class Launcher extends Activity public void setDynamicGridSize(DeviceProfile.GridSize size) { int gridSize = SettingsProvider.getIntCustomDefault(this, SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, 0); - if (gridSize != size.getValue()) { + boolean customValuesChanged = false; + if (gridSize == size.getValue() && size == DeviceProfile.GridSize.Custom) { + int tempRows = SettingsProvider.getIntCustomDefault(this, + SettingsProvider.SETTINGS_UI_HOMESCREEN_ROWS, (int)mGrid.numRows); + int tempColumns = SettingsProvider.getIntCustomDefault(this, + SettingsProvider.SETTINGS_UI_HOMESCREEN_COLUMNS, (int)mGrid.numColumns); + if (tempColumns != (int) mGrid.numColumns || tempRows != (int) mGrid.numRows) { + customValuesChanged = true; + } + } + + if (gridSize != size.getValue() || customValuesChanged) { SettingsProvider.putInt(this, SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, size.getValue()); @@ -1644,6 +1697,9 @@ public class Launcher extends Activity mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content); mAppsCustomizeContent.setup(this, dragController); + // Setup AppDrawer + setupAppDrawer(); + // Setup the drag controller (drop targets have to be added in reverse order in priority) dragController.setDragScoller(mWorkspace); dragController.setScrollView(mDragLayer); @@ -1669,6 +1725,20 @@ public class Launcher extends Activity } } + private void setupAppDrawer() { + if (mAppDrawer == null) { + FrameLayout view = (FrameLayout) findViewById(R.id.app_drawer_container); + mAppDrawer = (RecyclerView) view.findViewById(R.id.app_drawer_recyclerview); + mAppDrawer.setLayoutManager(new LinearLayoutManager(this)); + if (mAppDrawerAdapter == null) { + initializeAdapter(); + } + mAppDrawer.setHasFixedSize(true); + mAppDrawer.setAdapter(mAppDrawerAdapter); + initializeScrubber(); + } + } + /** * Sets the all apps button. This method is called from {@link Hotseat}. */ @@ -2203,11 +2273,6 @@ public class Launcher extends Activity imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } - // Reset the apps customize page - if (!alreadyOnHome && mAppsCustomizeTabHost != null) { - mAppsCustomizeTabHost.reset(); - } - onHomeIntent(); } @@ -3205,7 +3270,7 @@ public class Launcher extends Activity return false; } - boolean startActivitySafely(View v, Intent intent, Object tag) { + public boolean startActivitySafely(View v, Intent intent, Object tag) { boolean success = false; if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) { Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show(); @@ -3583,7 +3648,7 @@ public class Launcher extends Activity } boolean material = Utilities.isLmpOrAbove(); - + boolean drawer = mDrawerType == AppDrawerListAdapter.DrawerType.Drawer; final Resources res = getResources(); final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime); @@ -3594,7 +3659,13 @@ public class Launcher extends Activity final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); final View fromView = mWorkspace; - final AppsCustomizeTabHost toView = mAppsCustomizeTabHost; + final View toView; + + if (drawer && contentType == AppsCustomizePagedView.ContentType.Applications) { + toView = findViewById(R.id.app_drawer_container); + } else { + toView = mAppsCustomizeTabHost; + } final ArrayList<View> layerViews = new ArrayList<View>(); @@ -3616,7 +3687,8 @@ public class Launcher extends Activity final AppsCustomizePagedView content = (AppsCustomizePagedView) toView.findViewById(R.id.apps_customize_pane_content); - final View page = content.getPageAt(content.getCurrentPage()); + final View page = content != null ? content.getPageAt(content.getCurrentPage()) + : toView.findViewById(R.id.app_drawer_view); final View revealView = toView.findViewById(R.id.fake_page); final float initialPanelAlpha = 1f; @@ -3625,11 +3697,19 @@ public class Launcher extends Activity if (isWidgetTray) { revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); } else { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); + if (drawer) { + revealView.setBackgroundColor(res.getColor(R.color.app_drawer_background)); + } else { + revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); + } } // Hide the real page background, and swap in the fake one - content.setPageBackgroundsVisible(false); + if (content != null) { + content.setPageBackgroundsVisible(false); + } else { + toView.setBackgroundColor(Color.TRANSPARENT); + } revealView.setVisibility(View.VISIBLE); // We need to hide this view as the animation start will be posted. revealView.setAlpha(0); @@ -3674,6 +3754,11 @@ public class Launcher extends Activity mStateAnimation.play(panelAlphaAndDrift); + final View drawerContent = content == null ? + toView.findViewById(R.id.app_drawer_recyclerview) : null; + final View drawerScrubber = content == null ? + toView.findViewById(R.id.scrubber_container) : null; + if (page != null) { page.setVisibility(View.VISIBLE); page.setLayerType(View.LAYER_TYPE_HARDWARE, null); @@ -3692,14 +3777,32 @@ public class Launcher extends Activity itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); itemsAlpha.setStartDelay(itemsAlphaStagger); mStateAnimation.play(itemsAlpha); + + if (drawerContent != null) { + drawerContent.setTranslationY(toView.getHeight()); + ObjectAnimator slideIn = ObjectAnimator.ofFloat(drawerContent, + "translationY", 1000, 0); + slideIn.setInterpolator(new OvershootInterpolator(OVERSHOOT_TENSION)); + slideIn.setStartDelay(revealDuration / 2); + mStateAnimation.play(slideIn); + } + if (drawerScrubber != null) { + drawerScrubber.setAlpha(0f); + ObjectAnimator fadeIn = ObjectAnimator.ofFloat(drawerScrubber, + "alpha", 0f, 1f); + fadeIn.setStartDelay(revealDuration / 2); + mStateAnimation.play(fadeIn); + } } View pageIndicators = toView.findViewById(R.id.apps_customize_page_indicator); - pageIndicators.setAlpha(0.01f); - ObjectAnimator indicatorsAlpha = - ObjectAnimator.ofFloat(pageIndicators, "alpha", 1f); - indicatorsAlpha.setDuration(revealDuration); - mStateAnimation.play(indicatorsAlpha); + if (pageIndicators != null) { + pageIndicators.setAlpha(0.01f); + ObjectAnimator indicatorsAlpha = + ObjectAnimator.ofFloat(pageIndicators, "alpha", 1f); + indicatorsAlpha.setDuration(revealDuration); + mStateAnimation.play(indicatorsAlpha); + } if (material) { final View allApps = getAllAppsButton(); @@ -3737,7 +3840,11 @@ public class Launcher extends Activity if (page != null) { page.setLayerType(View.LAYER_TYPE_NONE, null); } - content.setPageBackgroundsVisible(true); + if (content != null) { + content.setPageBackgroundsVisible(true); + } else { + toView.setBackgroundColor(res.getColor(R.color.app_drawer_background)); + } // Hide the search bar if (mSearchDropTargetBar != null) { @@ -3820,7 +3927,8 @@ public class Launcher extends Activity } boolean material = Utilities.isLmpOrAbove(); - Resources res = getResources(); + boolean drawer = mDrawerType == AppDrawerListAdapter.DrawerType.Drawer; + final Resources res = getResources(); final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime); final int fadeOutDuration = res.getInteger(R.integer.config_appsCustomizeFadeOutTime); @@ -3830,7 +3938,15 @@ public class Launcher extends Activity final float scaleFactor = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); - final View fromView = mAppsCustomizeTabHost; + final View fromView; + + if (drawer && mAppsCustomizeContent.getContentType() + != AppsCustomizePagedView.ContentType.Widgets) { + fromView = (FrameLayout) findViewById(R.id.app_drawer_container); + } else { + fromView = mAppsCustomizeTabHost; + } + final View toView = mWorkspace; Animator workspaceAnim = null; final ArrayList<View> layerViews = new ArrayList<View>(); @@ -3856,10 +3972,11 @@ public class Launcher extends Activity final AppsCustomizePagedView content = (AppsCustomizePagedView) fromView.findViewById(R.id.apps_customize_pane_content); - final View page = content.getPageAt(content.getNextPage()); + final View page = content != null ? content.getPageAt(content.getNextPage()) + : fromView.findViewById(R.id.app_drawer_view); // We need to hide side pages of the Apps / Widget tray to avoid some ugly edge cases - int count = content.getChildCount(); + int count = content != null ? content.getChildCount() : 0; for (int i = 0; i < count; i++) { View child = content.getChildAt(i); if (child != page) { @@ -3872,14 +3989,20 @@ public class Launcher extends Activity // don't perform all these no-op animations. In particularly, this was causing // the all-apps button to pop in and out. if (fromView.getVisibility() == View.VISIBLE) { - AppsCustomizePagedView.ContentType contentType = content.getContentType(); + AppsCustomizePagedView.ContentType contentType = + mAppsCustomizeContent.getContentType(); final boolean isWidgetTray = contentType == AppsCustomizePagedView.ContentType.Widgets; if (isWidgetTray) { revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); } else { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); + if (drawer) { + revealView.setBackgroundColor(res.getColor( + R.color.app_drawer_background)); + } else { + revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); + } } int width = revealView.getMeasuredWidth(); @@ -3888,7 +4011,11 @@ public class Launcher extends Activity // Hide the real page background, and swap in the fake one revealView.setVisibility(View.VISIBLE); - content.setPageBackgroundsVisible(false); + if (content != null) { + content.setPageBackgroundsVisible(false); + } else { + fromView.setBackgroundColor(Color.TRANSPARENT); + } final View allAppsButton = getAllAppsButton(); revealView.setTranslationY(0); @@ -3938,6 +4065,9 @@ public class Launcher extends Activity mStateAnimation.play(panelAlpha); } + final View drawerScrubber = content == null ? + fromView.findViewById(R.id.scrubber_container) : null; + if (page != null) { page.setLayerType(View.LAYER_TYPE_HARDWARE, null); @@ -3954,15 +4084,24 @@ public class Launcher extends Activity itemsAlpha.setDuration(100); itemsAlpha.setInterpolator(decelerateInterpolator); mStateAnimation.play(itemsAlpha); + + if (drawerScrubber != null) { + drawerScrubber.setAlpha(1f); + ObjectAnimator fadeOut = ObjectAnimator.ofFloat(drawerScrubber, + "alpha", 1f, 0f); + mStateAnimation.play(fadeOut); + } } View pageIndicators = fromView.findViewById(R.id.apps_customize_page_indicator); - pageIndicators.setAlpha(1f); - ObjectAnimator indicatorsAlpha = - LauncherAnimUtils.ofFloat(pageIndicators, "alpha", 0f); - indicatorsAlpha.setDuration(revealDuration); - indicatorsAlpha.setInterpolator(new DecelerateInterpolator(1.5f)); - mStateAnimation.play(indicatorsAlpha); + if (pageIndicators != null) { + pageIndicators.setAlpha(1f); + ObjectAnimator indicatorsAlpha = + LauncherAnimUtils.ofFloat(pageIndicators, "alpha", 0f); + indicatorsAlpha.setDuration(revealDuration); + indicatorsAlpha.setInterpolator(new DecelerateInterpolator(1.5f)); + mStateAnimation.play(indicatorsAlpha); + } width = revealView.getMeasuredWidth(); @@ -4011,9 +4150,13 @@ public class Launcher extends Activity if (page != null) { page.setLayerType(View.LAYER_TYPE_NONE, null); } - content.setPageBackgroundsVisible(true); + if (content != null) { + content.setPageBackgroundsVisible(true); + } else { + fromView.setBackgroundColor(res.getColor(R.color.app_drawer_background)); + } // Unhide side pages - int count = content.getChildCount(); + int count = content != null ? content.getChildCount() : 0; for (int i = 0; i < count; i++) { View child = content.getChildAt(i); child.setVisibility(View.VISIBLE); @@ -4025,7 +4168,9 @@ public class Launcher extends Activity page.setTranslationY(0); page.setAlpha(1); } - content.setCurrentPage(content.getNextPage()); + if (content != null) { + content.setCurrentPage(content.getNextPage()); + } mAppsCustomizeContent.updateCurrentPageScroll(); } @@ -4676,6 +4821,7 @@ public class Launcher extends Activity if (!LauncherAppState.isDisableAllApps() && addedApps != null && mAppsCustomizeContent != null) { mAppsCustomizeContent.addApps(addedApps); + mAppDrawerAdapter.addApps(addedApps); } } @@ -5059,6 +5205,9 @@ public class Launcher extends Activity LauncherModel.getSortedWidgetsAndShortcuts(this)); } } else { + if (mAppDrawerAdapter != null) { + mAppDrawerAdapter.setApps(apps); + } if (mAppsCustomizeContent != null) { mAppsCustomizeContent.setApps(apps); mAppsCustomizeContent.onPackagesUpdated( @@ -5089,6 +5238,7 @@ public class Launcher extends Activity if (!LauncherAppState.isDisableAllApps() && mAppsCustomizeContent != null) { mAppsCustomizeContent.updateApps(apps); + mAppDrawerAdapter.updateApps(apps); } } @@ -5168,6 +5318,7 @@ public class Launcher extends Activity if (!LauncherAppState.isDisableAllApps() && mAppsCustomizeContent != null) { mAppsCustomizeContent.removeApps(appInfos); + mAppDrawerAdapter.removeApps(appInfos); } } @@ -5425,16 +5576,24 @@ public class Launcher extends Activity if (mWorkspace != null) mWorkspace.setAlpha(1f); if (mHotseat != null) mHotseat.setAlpha(1f); if (mPageIndicators != null) mPageIndicators.setAlpha(1f); - if (mSearchDropTargetBar != null) mSearchDropTargetBar.showSearchBar(false); + showSearch(); } void hideWorkspaceSearchAndHotseat() { if (mWorkspace != null) mWorkspace.setAlpha(0f); if (mHotseat != null) mHotseat.setAlpha(0f); if (mPageIndicators != null) mPageIndicators.setAlpha(0f); + hideSearch(); + } + + void hideSearch() { if (mSearchDropTargetBar != null) mSearchDropTargetBar.hideSearchBar(false); } + void showSearch() { + if (mSearchDropTargetBar != null) mSearchDropTargetBar.showSearchBar(false); + } + public ItemInfo createAppDragInfo(Intent appLaunchIntent) { // Called from search suggestion, not supported in other profiles. final UserHandleCompat myUser = UserHandleCompat.myUserHandle(); @@ -5623,9 +5782,11 @@ public class Launcher extends Activity mGrid.layout(Launcher.this); // Synchronized reload + mModel.stopLoader(); //make sure the loader isn't running mModel.startLoader(true, page); mWorkspace.updateCustomContentVisibility(); + mAppDrawerAdapter.reset(); } public void setUpdateDynamicGrid() { diff --git a/src/com/android/launcher3/OverviewSettingsPanel.java b/src/com/android/launcher3/OverviewSettingsPanel.java index 983c5aaf6..4ea86a31c 100644 --- a/src/com/android/launcher3/OverviewSettingsPanel.java +++ b/src/com/android/launcher3/OverviewSettingsPanel.java @@ -49,6 +49,7 @@ public class OverviewSettingsPanel { res.getString(R.string.grid_size_text)}; String[] valuesDrawer = new String[] { + res.getString(R.string.drawer_type), res.getString(R.string.scroll_effect_text), res.getString(R.string.drawer_sorting_text), res.getString(R.string.icon_labels)}; diff --git a/src/com/android/launcher3/PagedViewIcon.java b/src/com/android/launcher3/PagedViewIcon.java new file mode 100644 index 000000000..660251e31 --- /dev/null +++ b/src/com/android/launcher3/PagedViewIcon.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2010 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.launcher3; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.TextView; + +/** + * An icon on a PagedView, specifically for items in the launcher's paged view (with compound + * drawables on the top). + */ +public class PagedViewIcon extends TextView { + /** A simple callback interface to allow a PagedViewIcon to notify when it has been pressed */ + public static interface PressedCallback { + void iconPressed(PagedViewIcon icon); + } + + @SuppressWarnings("unused") + private static final String TAG = "PagedViewIcon"; + static final float PRESS_ALPHA = 0.4f; + + private PagedViewIcon.PressedCallback mPressedCallback; + private boolean mLockDrawableState = false; + + private Bitmap mIcon; + + public PagedViewIcon(Context context) { + this(context, null); + } + + public PagedViewIcon(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewIcon(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void onFinishInflate() { + super.onFinishInflate(); + + // Ensure we are using the right text size + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); + } + + public void applyFromApplicationInfo(AppInfo info, boolean scaleUp, + PagedViewIcon.PressedCallback cb) { + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + + mIcon = info.iconBitmap; + mPressedCallback = cb; + Drawable icon = Utilities.createIconDrawable(mIcon); + icon.setBounds(0, 0, grid.allAppsIconSizePx, grid.allAppsIconSizePx); + setCompoundDrawables(null, icon, null, null); + setCompoundDrawablePadding(grid.iconDrawablePaddingPx); + setText(info.title); + setTag(info); + } + + public void lockDrawableState() { + mLockDrawableState = true; + } + + public void resetDrawableState() { + mLockDrawableState = false; + post(new Runnable() { + @Override + public void run() { + refreshDrawableState(); + } + }); + } + + protected void drawableStateChanged() { + super.drawableStateChanged(); + + // We keep in the pressed state until resetDrawableState() is called to reset the press + // feedback + if (isPressed()) { + setAlpha(PRESS_ALPHA); + if (mPressedCallback != null) { + mPressedCallback.iconPressed(this); + } + } else if (!mLockDrawableState) { + setAlpha(1f); + } + } + + @Override + public void draw(Canvas canvas) { + // If text is transparent, don't draw any shadow + if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) { + getPaint().clearShadowLayer(); + super.draw(canvas); + return; + } + + // We enhance the shadow by drawing the shadow twice + getPaint().setShadowLayer(BubbleTextView.SHADOW_LARGE_RADIUS, 0.0f, + BubbleTextView.SHADOW_Y_OFFSET, BubbleTextView.SHADOW_LARGE_COLOUR); + super.draw(canvas); + canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(), + getScrollX() + getWidth(), + getScrollY() + getHeight(), Region.Op.INTERSECT); + getPaint().setShadowLayer(BubbleTextView.SHADOW_SMALL_RADIUS, 0.0f, 0.0f, + BubbleTextView.SHADOW_SMALL_COLOUR); + super.draw(canvas); + canvas.restore(); + } +} diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index addd74cfc..fcd4b8587 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -41,6 +41,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.os.Build; +import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -558,4 +559,11 @@ public final class Utilities { } return null; } + + public static float convertDpToPixel(float dp, Context context){ + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + float px = dp * (metrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT); + return px; + } } diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index 5aa719027..d496c1c08 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -12,6 +12,7 @@ import android.database.sqlite.SQLiteCantOpenDatabaseException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDiskIOException; import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteReadOnlyDatabaseException; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; @@ -379,6 +380,7 @@ public class WidgetPreviewLoader { db.delete(CacheDb.TABLE_NAME, null, null); } catch (SQLiteDiskIOException e) { } catch (SQLiteCantOpenDatabaseException e) { + } catch (SQLiteReadOnlyDatabaseException e) { dumpOpenFiles(); throw e; } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index a93fff47b..ee812aa0e 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -2567,7 +2567,7 @@ public class Workspace extends SmoothPagedView } mLauncher.updateVoiceButtonProxyVisible(false); - if (stateIsNormal) { + if (stateIsNormal || stateIsNormalHidden) { animateBackgroundGradient(0f, animated); } else { animateBackgroundGradient(getResources().getInteger( diff --git a/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java b/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java index 4e25a3b15..c4b5dee47 100644 --- a/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java +++ b/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java @@ -6,7 +6,9 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; +import android.graphics.Color; import android.graphics.Typeface; +import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; @@ -14,6 +16,7 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.TextView; +import com.android.launcher3.AppDrawerListAdapter; import com.android.launcher3.AppsCustomizePagedView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; @@ -24,6 +27,8 @@ import com.android.launcher3.settings.SettingsProvider; public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { private static final int PARTITION_TAG = 0; private static final int POSITION_TAG = 1; + private static final float ENABLED_ALPHA = 1f; + private static final float DISABLED_ALPHA = 1f; private Launcher mLauncher; private Context mContext; @@ -90,6 +95,7 @@ public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { Resources res = mLauncher.getResources(); + boolean current = false; String state = ""; @@ -131,14 +137,21 @@ public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { case OverviewSettingsPanel.DRAWER_SETTINGS_POSITION: switch (position) { case 0: - state = mLauncher.getAppsCustomizeTransitionEffect(); - state = mapEffectToValue(state); - ((TextView) v.findViewById(R.id.item_state)).setText(state); + updateDrawerTypeSettingsItem(v); break; case 1: - updateDrawerSortSettingsItem(v); + if (!setDisabled(v)) { + state = mLauncher.getAppsCustomizeTransitionEffect(); + state = mapEffectToValue(state); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + } break; case 2: + if (!setDisabled(v)) { + updateDrawerSortSettingsItem(v); + } + break; + case 3: current = SettingsProvider.getBoolean(mContext, SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, R.bool.preferences_interface_drawer_hide_icon_labels_default); @@ -209,6 +222,20 @@ public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { ((TextView) v.findViewById(R.id.item_state)).setText(state); } + public void updateDrawerTypeSettingsItem(View v) { + String state = ""; + AppDrawerListAdapter.DrawerType type = mLauncher.getDrawerType(); + switch (type) { + case Drawer: + state = mLauncher.getResources().getString(R.string.drawer_type_drawer); + break; + case Pager: + state = mLauncher.getResources().getString(R.string.drawer_type_pager); + break; + } + ((TextView) v.findViewById(R.id.item_state)).setText(state); + } + public void updateDynamicGridSizeSettingsItem(View v) { DeviceProfile.GridSize gridSize = DeviceProfile.GridSize.getModeForValue( SettingsProvider.getIntCustomDefault(mLauncher, @@ -295,12 +322,17 @@ public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { case OverviewSettingsPanel.DRAWER_SETTINGS_POSITION: switch (position) { case 0: - mLauncher.onClickTransitionEffectButton(v, true); + onClickDrawerTypeButton(); break; case 1: - onClickSortButton(); + mLauncher.onClickTransitionEffectButton(v, true); + break; case 2: + onClickSortButton(); + + break; + case 3: onIconLabelsBooleanChanged(v, SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, R.bool.preferences_interface_drawer_hide_icon_labels_default); @@ -384,4 +416,44 @@ public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { notifyDataSetChanged(); } + + private void onClickDrawerTypeButton() { + int type = SettingsProvider.getInt(mLauncher, + SettingsProvider.SETTINGS_UI_DRAWER_TYPE, + R.integer.preferences_interface_drawer_type_default); + + type = (type + 1) % AppDrawerListAdapter.DrawerType.values().length; + SettingsProvider.putInt(mLauncher, SettingsProvider.SETTINGS_UI_DRAWER_TYPE, type); + + mLauncher.updateDrawerType(); + + notifyDataSetChanged(); + } + + private boolean setDisabled(View v) { + TextView itemState = ((TextView) v.findViewById(R.id.item_state)); + TextView itemName = ((TextView) v.findViewById(R.id.item_name)); + + AppDrawerListAdapter.DrawerType type = mLauncher.getDrawerType(); + + boolean isDisabled = false; + + switch (type) { + case Drawer: + itemState.setAlpha(DISABLED_ALPHA); + itemState.setText(mLauncher.getResources() + .getString(R.string.setting_state_disabled)); + itemName.setAlpha(DISABLED_ALPHA); + v.setEnabled(false); + isDisabled = true; + break; + case Pager: + itemState.setAlpha(ENABLED_ALPHA); + itemName.setAlpha(ENABLED_ALPHA); + v.setEnabled(true); + break; + } + + return isDisabled; + } } diff --git a/src/com/android/launcher3/locale/HanziToPinyin.java b/src/com/android/launcher3/locale/HanziToPinyin.java new file mode 100644 index 000000000..9e398fac0 --- /dev/null +++ b/src/com/android/launcher3/locale/HanziToPinyin.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2011 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.launcher3.locale; + +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; + +import libcore.icu.Transliterator; + +/** + * An object to convert Chinese character to its corresponding pinyin string. + * For characters with multiple possible pinyin string, only one is selected + * according to ICU Transliterator class. Polyphone is not supported in this + * implementation. + */ +public class HanziToPinyin { + private static final String TAG = "HanziToPinyin"; + + private static HanziToPinyin sInstance; + private Transliterator mPinyinTransliterator; + private Transliterator mAsciiTransliterator; + + public static class Token { + /** + * Separator between target string for each source char + */ + public static final String SEPARATOR = " "; + + public static final int LATIN = 1; + public static final int PINYIN = 2; + public static final int UNKNOWN = 3; + + public Token() { + } + + public Token(int type, String source, String target) { + this.type = type; + this.source = source; + this.target = target; + } + + /** + * Type of this token, ASCII, PINYIN or UNKNOWN. + */ + public int type; + /** + * Original string before translation. + */ + public String source; + /** + * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is + * original string in source. + */ + public String target; + } + + private HanziToPinyin() { + try { + mPinyinTransliterator = new Transliterator("Han-Latin/Names; Latin-Ascii; Any-Upper"); + mAsciiTransliterator = new Transliterator("Latin-Ascii"); + } catch (RuntimeException e) { + Log.w(TAG, "Han-Latin/Names transliterator data is missing," + + " HanziToPinyin is disabled"); + } + } + + public boolean hasChineseTransliterator() { + return mPinyinTransliterator != null; + } + + public static HanziToPinyin getInstance() { + synchronized (HanziToPinyin.class) { + if (sInstance == null) { + sInstance = new HanziToPinyin(); + } + return sInstance; + } + } + + private void tokenize(char character, Token token) { + token.source = Character.toString(character); + + // ASCII + if (character < 128) { + token.type = Token.LATIN; + token.target = token.source; + return; + } + + // Extended Latin. Transcode these to ASCII equivalents + if (character < 0x250 || (0x1e00 <= character && character < 0x1eff)) { + token.type = Token.LATIN; + token.target = mAsciiTransliterator == null ? token.source : + mAsciiTransliterator.transliterate(token.source); + return; + } + + token.type = Token.PINYIN; + token.target = mPinyinTransliterator.transliterate(token.source); + if (TextUtils.isEmpty(token.target) || + TextUtils.equals(token.source, token.target)) { + token.type = Token.UNKNOWN; + token.target = token.source; + } + } + + public String transliterate(final String input) { + if (!hasChineseTransliterator() || TextUtils.isEmpty(input)) { + return null; + } + return mPinyinTransliterator.transliterate(input); + } + + /** + * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without + * space will be put into a Token, One Hanzi character which has pinyin will be treated as a + * Token. If there is no Chinese transliterator, the empty token array is returned. + */ + public ArrayList<Token> getTokens(final String input) { + ArrayList<Token> tokens = new ArrayList<Token>(); + if (!hasChineseTransliterator() || TextUtils.isEmpty(input)) { + // return empty tokens. + return tokens; + } + + final int inputLength = input.length(); + final StringBuilder sb = new StringBuilder(); + int tokenType = Token.LATIN; + Token token = new Token(); + + // Go through the input, create a new token when + // a. Token type changed + // b. Get the Pinyin of current charater. + // c. current character is space. + for (int i = 0; i < inputLength; i++) { + final char character = input.charAt(i); + if (Character.isSpaceChar(character)) { + if (sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + } else { + tokenize(character, token); + if (token.type == Token.PINYIN) { + if (sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + tokens.add(token); + token = new Token(); + } else { + if (tokenType != token.type && sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + sb.append(token.target); + } + tokenType = token.type; + } + } + if (sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + return tokens; + } + + private void addToken( + final StringBuilder sb, final ArrayList<Token> tokens, final int tokenType) { + String str = sb.toString(); + tokens.add(new Token(tokenType, str, str)); + sb.setLength(0); + } +} diff --git a/src/com/android/launcher3/locale/LocaleSet.java b/src/com/android/launcher3/locale/LocaleSet.java new file mode 100644 index 000000000..34634ab7e --- /dev/null +++ b/src/com/android/launcher3/locale/LocaleSet.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2014 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.launcher3.locale; + +import android.text.TextUtils; +import com.google.common.annotations.VisibleForTesting; +import java.util.Locale; + +public class LocaleSet { + private static final String CHINESE_LANGUAGE = Locale.CHINESE.getLanguage().toLowerCase(); + private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase(); + private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase(); + + private static class LocaleWrapper { + private final Locale mLocale; + private final String mLanguage; + private final boolean mLocaleIsCJK; + + private static boolean isLanguageCJK(String language) { + return CHINESE_LANGUAGE.equals(language) || + JAPANESE_LANGUAGE.equals(language) || + KOREAN_LANGUAGE.equals(language); + } + + public LocaleWrapper(Locale locale) { + mLocale = locale; + if (mLocale != null) { + mLanguage = mLocale.getLanguage().toLowerCase(); + mLocaleIsCJK = isLanguageCJK(mLanguage); + } else { + mLanguage = null; + mLocaleIsCJK = false; + } + } + + public boolean hasLocale() { + return mLocale != null; + } + + public Locale getLocale() { + return mLocale; + } + + public boolean isLocale(Locale locale) { + return mLocale == null ? (locale == null) : mLocale.equals(locale); + } + + public boolean isLocaleCJK() { + return mLocaleIsCJK; + } + + public boolean isLanguage(String language) { + return mLanguage == null ? (language == null) + : mLanguage.equalsIgnoreCase(language); + } + + public String toString() { + return mLocale != null ? mLocale.toLanguageTag() : "(null)"; + } + } + + public static LocaleSet getDefault() { + return new LocaleSet(Locale.getDefault()); + } + + public LocaleSet(Locale locale) { + this(locale, null); + } + + /** + * Returns locale set for a given set of IETF BCP-47 tags separated by ';'. + * BCP-47 tags are what is used by ICU 52's toLanguageTag/forLanguageTag + * methods to represent individual Locales: "en-US" for Locale.US, + * "zh-CN" for Locale.CHINA, etc. So eg "en-US;zh-CN" specifies the locale + * set LocaleSet(Locale.US, Locale.CHINA). + * + * @param localeString One or more BCP-47 tags separated by ';'. + * @return LocaleSet for specified locale string, or default set if null + * or unable to parse. + */ + public static LocaleSet getLocaleSet(String localeString) { + // Locale.toString() generates strings like "en_US" and "zh_CN_#Hans". + // Locale.toLanguageTag() generates strings like "en-US" and "zh-Hans-CN". + // We can only parse language tags. + if (localeString != null && localeString.indexOf('_') == -1) { + final String[] locales = localeString.split(";"); + final Locale primaryLocale = Locale.forLanguageTag(locales[0]); + // ICU tags undefined/unparseable locales "und" + if (primaryLocale != null && + !TextUtils.equals(primaryLocale.toLanguageTag(), "und")) { + if (locales.length > 1 && locales[1] != null) { + final Locale secondaryLocale = Locale.forLanguageTag(locales[1]); + if (secondaryLocale != null && + !TextUtils.equals(secondaryLocale.toLanguageTag(), "und")) { + return new LocaleSet(primaryLocale, secondaryLocale); + } + } + return new LocaleSet(primaryLocale); + } + } + return getDefault(); + } + + private final LocaleWrapper mPrimaryLocale; + private final LocaleWrapper mSecondaryLocale; + + public LocaleSet(Locale primaryLocale, Locale secondaryLocale) { + mPrimaryLocale = new LocaleWrapper(primaryLocale); + mSecondaryLocale = new LocaleWrapper( + mPrimaryLocale.equals(secondaryLocale) ? null : secondaryLocale); + } + + public LocaleSet normalize() { + final Locale primaryLocale = getPrimaryLocale(); + if (primaryLocale == null) { + return getDefault(); + } + Locale secondaryLocale = getSecondaryLocale(); + // disallow both locales with same language (redundant and/or conflicting) + // disallow both locales CJK (conflicting rules) + if (secondaryLocale == null || + isPrimaryLanguage(secondaryLocale.getLanguage()) || + (isPrimaryLocaleCJK() && isSecondaryLocaleCJK())) { + return new LocaleSet(primaryLocale); + } + // unnecessary to specify English as secondary locale (redundant) + if (isSecondaryLanguage(Locale.ENGLISH.getLanguage())) { + return new LocaleSet(primaryLocale); + } + return this; + } + + public boolean hasSecondaryLocale() { + return mSecondaryLocale.hasLocale(); + } + + public Locale getPrimaryLocale() { + return mPrimaryLocale.getLocale(); + } + + public Locale getSecondaryLocale() { + return mSecondaryLocale.getLocale(); + } + + public boolean isPrimaryLocale(Locale locale) { + return mPrimaryLocale.isLocale(locale); + } + + public boolean isSecondaryLocale(Locale locale) { + return mSecondaryLocale.isLocale(locale); + } + + private static final String SCRIPT_SIMPLIFIED_CHINESE = "Hans"; + private static final String SCRIPT_TRADITIONAL_CHINESE = "Hant"; + + @VisibleForTesting + public static boolean isLocaleSimplifiedChinese(Locale locale) { + // language must match + if (locale == null || !TextUtils.equals(locale.getLanguage(), CHINESE_LANGUAGE)) { + return false; + } + // script is optional but if present must match + if (!TextUtils.isEmpty(locale.getScript())) { + return locale.getScript().equals(SCRIPT_SIMPLIFIED_CHINESE); + } + // if no script, must match known country + return locale.equals(Locale.SIMPLIFIED_CHINESE); + } + + public boolean isPrimaryLocaleSimplifiedChinese() { + return isLocaleSimplifiedChinese(getPrimaryLocale()); + } + + public boolean isSecondaryLocaleSimplifiedChinese() { + return isLocaleSimplifiedChinese(getSecondaryLocale()); + } + + @VisibleForTesting + public static boolean isLocaleTraditionalChinese(Locale locale) { + // language must match + if (locale == null || !TextUtils.equals(locale.getLanguage(), CHINESE_LANGUAGE)) { + return false; + } + // script is optional but if present must match + if (!TextUtils.isEmpty(locale.getScript())) { + return locale.getScript().equals(SCRIPT_TRADITIONAL_CHINESE); + } + // if no script, must match known country + return locale.equals(Locale.TRADITIONAL_CHINESE); + } + + public boolean isPrimaryLocaleTraditionalChinese() { + return isLocaleTraditionalChinese(getPrimaryLocale()); + } + + public boolean isSecondaryLocaleTraditionalChinese() { + return isLocaleTraditionalChinese(getSecondaryLocale()); + } + + public boolean isPrimaryLocaleCJK() { + return mPrimaryLocale.isLocaleCJK(); + } + + public boolean isSecondaryLocaleCJK() { + return mSecondaryLocale.isLocaleCJK(); + } + + public boolean isPrimaryLanguage(String language) { + return mPrimaryLocale.isLanguage(language); + } + + public boolean isSecondaryLanguage(String language) { + return mSecondaryLocale.isLanguage(language); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (object instanceof LocaleSet) { + final LocaleSet other = (LocaleSet) object; + return other.isPrimaryLocale(mPrimaryLocale.getLocale()) + && other.isSecondaryLocale(mSecondaryLocale.getLocale()); + } + return false; + } + + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(mPrimaryLocale.toString()); + if (hasSecondaryLocale()) { + builder.append(";"); + builder.append(mSecondaryLocale.toString()); + } + return builder.toString(); + } +} diff --git a/src/com/android/launcher3/locale/LocaleSetManager.java b/src/com/android/launcher3/locale/LocaleSetManager.java new file mode 100644 index 000000000..b058718f3 --- /dev/null +++ b/src/com/android/launcher3/locale/LocaleSetManager.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * Copyright (C) 2009 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.launcher3.locale; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.Locale; + +import libcore.icu.ICU; + +public class LocaleSetManager { + private static final String TAG = LocaleSetManager.class.getSimpleName(); + + private LocaleSet mCurrentLocales; + private final Context mContext; + + public LocaleSetManager(final Context context) { + mContext = context; + } + + /** + * Sets up the locale set + * @param localeSet value to set it to + */ + public void updateLocaleSet(LocaleSet localeSet) { + Log.d(TAG, "Locale Changed from: " + mCurrentLocales + " to " + localeSet); + mCurrentLocales = localeSet; + LocaleUtils.getInstance().setLocales(mCurrentLocales); + } + + /** + * This takes an old and new locale set and creates a combined locale set. If they share a + * primary then the old one is returned + * @return the combined locale set + */ + private static LocaleSet getCombinedLocaleSet(LocaleSet oldLocales, Locale newLocale) { + Locale prevLocale = null; + + if (oldLocales != null) { + prevLocale = oldLocales.getPrimaryLocale(); + // If primary locale is unchanged then no change to locale set. + if (newLocale.equals(prevLocale)) { + return oldLocales; + } + } + + // Otherwise, construct a new locale set based on the new locale + // and the previous primary locale. + return new LocaleSet(newLocale, prevLocale).normalize(); + } + + /** + * @return the system locale set + */ + public LocaleSet getSystemLocaleSet() { + final Locale curLocale = getLocale(); + return getCombinedLocaleSet(mCurrentLocales, curLocale); + } + + @VisibleForTesting + protected Locale getLocale() { + return Locale.getDefault(); + } +} diff --git a/src/com/android/launcher3/locale/LocaleUtils.java b/src/com/android/launcher3/locale/LocaleUtils.java new file mode 100644 index 000000000..cc8277a6c --- /dev/null +++ b/src/com/android/launcher3/locale/LocaleUtils.java @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2010 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.launcher3.locale; + +import android.provider.ContactsContract.FullNameStyle; +import android.provider.ContactsContract.PhoneticNameStyle; +import android.text.TextUtils; +import android.util.Log; + +import com.android.launcher3.locale.HanziToPinyin.Token; + +import com.google.common.annotations.VisibleForTesting; + +import java.lang.Character.UnicodeBlock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Set; + +import libcore.icu.AlphabeticIndex; +import libcore.icu.AlphabeticIndex.ImmutableIndex; +import libcore.icu.Transliterator; + +/** + * This utility class provides specialized handling for locale specific + * information: labels, name lookup keys. + * + * This class has been modified from ContactLocaleUtils.java for now to rip out + * Chinese/Japanese specific Alphabetic Indexers because the MediaProvider's sort + * is using a Collator sort which can result in confusing behavior, so for now we will + * simplify and batch up those results until we later support our own internal databases + * An example of what This is, if we have songs "Able", "Xylophone" and "上" in + * simplified chinese language The media provider would give it to us in that order sorted, + * but the ICU lib would return "A", "X", "S". Unless we write our own db or do our own sort + * there is no good easy solution + */ +public class LocaleUtils { + public static final String TAG = "LauncherLocale"; + + public static final Locale LOCALE_ARABIC = new Locale("ar"); + public static final Locale LOCALE_GREEK = new Locale("el"); + public static final Locale LOCALE_HEBREW = new Locale("he"); + // Serbian and Ukrainian labels are complementary supersets of Russian + public static final Locale LOCALE_SERBIAN = new Locale("sr"); + public static final Locale LOCALE_UKRAINIAN = new Locale("uk"); + public static final Locale LOCALE_THAI = new Locale("th"); + + /** + * This class is the default implementation and should be the base class + * for other locales. + * + * sortKey: same as name + * nameLookupKeys: none + * labels: uses ICU AlphabeticIndex for labels and extends by labeling + * phone numbers "#". Eg English labels are: [A-Z], #, " " + */ + private static class LocaleUtilsBase { + private static final String EMPTY_STRING = ""; + private static final String NUMBER_STRING = "#"; + + protected final ImmutableIndex mAlphabeticIndex; + private final int mAlphabeticIndexBucketCount; + private final int mNumberBucketIndex; + private final boolean mEnableSecondaryLocalePinyin; + + public LocaleUtilsBase(LocaleSet locales) { + // AlphabeticIndex.getBucketLabel() uses a binary search across + // the entire label set so care should be taken about growing this + // set too large. The following set determines for which locales + // we will show labels other than your primary locale. General rules + // of thumb for adding a locale: should be a supported locale; and + // should not be included if from a name it is not deterministic + // which way to label it (so eg Chinese cannot be added because + // the labeling of a Chinese character varies between Simplified, + // Traditional, and Japanese locales). Use English only for all + // Latin based alphabets. Ukrainian and Serbian are chosen for + // Cyrillic because their alphabets are complementary supersets + // of Russian. + final Locale secondaryLocale = locales.getSecondaryLocale(); + mEnableSecondaryLocalePinyin = locales.isSecondaryLocaleSimplifiedChinese(); + AlphabeticIndex ai = new AlphabeticIndex(locales.getPrimaryLocale()) + .setMaxLabelCount(300); + if (secondaryLocale != null) { + ai.addLabels(secondaryLocale); + } + mAlphabeticIndex = ai.addLabels(Locale.ENGLISH) + .addLabels(Locale.JAPANESE) + .addLabels(Locale.KOREAN) + .addLabels(LOCALE_THAI) + .addLabels(LOCALE_ARABIC) + .addLabels(LOCALE_HEBREW) + .addLabels(LOCALE_GREEK) + .addLabels(LOCALE_UKRAINIAN) + .addLabels(LOCALE_SERBIAN) + .getImmutableIndex(); + mAlphabeticIndexBucketCount = mAlphabeticIndex.getBucketCount(); + mNumberBucketIndex = mAlphabeticIndexBucketCount - 1; + } + + public String getSortKey(String name) { + return name; + } + + /** + * Returns the bucket index for the specified string. AlphabeticIndex + * sorts strings into buckets numbered in order from 0 to N, where the + * exact value of N depends on how many representative index labels are + * used in a particular locale. This routine adds one additional bucket + * for phone numbers. It attempts to detect phone numbers and shifts + * the bucket indexes returned by AlphabeticIndex in order to make room + * for the new # bucket, so the returned range becomes 0 to N+1. + */ + public int getBucketIndex(String name) { + boolean prefixIsNumeric = false; + final int length = name.length(); + int offset = 0; + while (offset < length) { + int codePoint = Character.codePointAt(name, offset); + // Ignore standard phone number separators and identify any + // string that otherwise starts with a number. + if (Character.isDigit(codePoint)) { + prefixIsNumeric = true; + break; + } else if (!Character.isSpaceChar(codePoint) && + codePoint != '+' && codePoint != '(' && + codePoint != ')' && codePoint != '.' && + codePoint != '-' && codePoint != '#') { + break; + } + offset += Character.charCount(codePoint); + } + if (prefixIsNumeric) { + return mNumberBucketIndex; + } + + /** + * TODO: ICU 52 AlphabeticIndex doesn't support Simplified Chinese + * as a secondary locale. Remove the following if that is added. + */ + if (mEnableSecondaryLocalePinyin) { + name = HanziToPinyin.getInstance().transliterate(name); + } + final int bucket = mAlphabeticIndex.getBucketIndex(name); + if (bucket < 0) { + return -1; + } + if (bucket >= mNumberBucketIndex) { + return bucket + 1; + } + return bucket; + } + + /** + * Returns the number of buckets in use (one more than AlphabeticIndex + * uses, because this class adds a bucket for phone numbers). + */ + public int getBucketCount() { + return mAlphabeticIndexBucketCount + 1; + } + + /** + * Returns the label for the specified bucket index if a valid index, + * otherwise returns an empty string. '#' is returned for the phone + * number bucket; for all others, the AlphabeticIndex label is returned. + */ + public String getBucketLabel(int bucketIndex) { + if (bucketIndex < 0 || bucketIndex >= getBucketCount()) { + return EMPTY_STRING; + } else if (bucketIndex == mNumberBucketIndex) { + return NUMBER_STRING; + } else if (bucketIndex > mNumberBucketIndex) { + --bucketIndex; + } + return mAlphabeticIndex.getBucketLabel(bucketIndex); + } + + @SuppressWarnings("unused") + public Iterator<String> getNameLookupKeys(String name, int nameStyle) { + return null; + } + + public ArrayList<String> getLabels() { + final int bucketCount = getBucketCount(); + final ArrayList<String> labels = new ArrayList<String>(bucketCount); + for(int i = 0; i < bucketCount; ++i) { + labels.add(getBucketLabel(i)); + } + return labels; + } + } + + /** + * Japanese specific locale overrides. + * + * sortKey: unchanged (same as name) + * nameLookupKeys: unchanged (none) + * labels: extends default labels by labeling unlabeled CJ characters + * with the Japanese character 他 ("misc"). Japanese labels are: + * あ, か, さ, た, な, は, ま, や, ら, わ, 他, [A-Z], #, " " + */ + private static class JapaneseContactUtils extends LocaleUtilsBase { + // \u4ed6 is Japanese character 他 ("misc") + private static final String JAPANESE_MISC_LABEL = "\u4ed6"; + private final int mMiscBucketIndex; + + public JapaneseContactUtils(LocaleSet locales) { + super(locales); + // Determine which bucket AlphabeticIndex is lumping unclassified + // Japanese characters into by looking up the bucket index for + // a representative Kanji/CJK unified ideograph (\u65e5 is the + // character '日'). + mMiscBucketIndex = super.getBucketIndex("\u65e5"); + } + + // Set of UnicodeBlocks for unified CJK (Chinese) characters and + // Japanese characters. This includes all code blocks that might + // contain a character used in Japanese (which is why unified CJK + // blocks are included but Korean Hangul and jamo are not). + private static final Set<Character.UnicodeBlock> CJ_BLOCKS; + static { + Set<UnicodeBlock> set = new HashSet<UnicodeBlock>(); + set.add(UnicodeBlock.HIRAGANA); + set.add(UnicodeBlock.KATAKANA); + set.add(UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS); + set.add(UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS); + set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS); + set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A); + set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B); + set.add(UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION); + set.add(UnicodeBlock.CJK_RADICALS_SUPPLEMENT); + set.add(UnicodeBlock.CJK_COMPATIBILITY); + set.add(UnicodeBlock.CJK_COMPATIBILITY_FORMS); + set.add(UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS); + set.add(UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT); + CJ_BLOCKS = Collections.unmodifiableSet(set); + } + + /** + * Helper routine to identify unlabeled Chinese or Japanese characters + * to put in a 'misc' bucket. + * + * @return true if the specified Unicode code point is Chinese or + * Japanese + */ + private static boolean isChineseOrJapanese(int codePoint) { + return CJ_BLOCKS.contains(UnicodeBlock.of(codePoint)); + } + + /** + * Returns the bucket index for the specified string. Adds an + * additional 'misc' bucket for Kanji characters to the base class set. + */ + @Override + public int getBucketIndex(String name) { + final int bucketIndex = super.getBucketIndex(name); + if ((bucketIndex == mMiscBucketIndex && + !isChineseOrJapanese(Character.codePointAt(name, 0))) || + bucketIndex > mMiscBucketIndex) { + return bucketIndex + 1; + } + return bucketIndex; + } + + /** + * Returns the number of buckets in use (one more than the base class + * uses, because this class adds a bucket for Kanji). + */ + @Override + public int getBucketCount() { + return super.getBucketCount() + 1; + } + + /** + * Returns the label for the specified bucket index if a valid index, + * otherwise returns an empty string. '他' is returned for unclassified + * Kanji; for all others, the label determined by the base class is + * returned. + */ + @Override + public String getBucketLabel(int bucketIndex) { + if (bucketIndex == mMiscBucketIndex) { + return JAPANESE_MISC_LABEL; + } else if (bucketIndex > mMiscBucketIndex) { + --bucketIndex; + } + return super.getBucketLabel(bucketIndex); + } + + @Override + public Iterator<String> getNameLookupKeys(String name, int nameStyle) { + // Hiragana and Katakana will be positively identified as Japanese. + if (nameStyle == PhoneticNameStyle.JAPANESE) { + return getRomajiNameLookupKeys(name); + } + return null; + } + + private static boolean mInitializedTransliterator; + private static Transliterator mJapaneseTransliterator; + + private static Transliterator getJapaneseTransliterator() { + synchronized(JapaneseContactUtils.class) { + if (!mInitializedTransliterator) { + mInitializedTransliterator = true; + Transliterator t = null; + try { + t = new Transliterator("Hiragana-Latin; Katakana-Latin;" + + " Latin-Ascii"); + } catch (RuntimeException e) { + Log.w(TAG, "Hiragana/Katakana-Latin transliterator data" + + " is missing"); + } + mJapaneseTransliterator = t; + } + return mJapaneseTransliterator; + } + } + + public static Iterator<String> getRomajiNameLookupKeys(String name) { + final Transliterator t = getJapaneseTransliterator(); + if (t == null) { + return null; + } + final String romajiName = t.transliterate(name); + if (TextUtils.isEmpty(romajiName) || + TextUtils.equals(name, romajiName)) { + return null; + } + final HashSet<String> keys = new HashSet<String>(); + keys.add(romajiName); + return keys.iterator(); + } + } + + /** + * Simplified Chinese specific locale overrides. Uses ICU Transliterator + * for generating pinyin transliteration. + * + * sortKey: unchanged (same as name) + * nameLookupKeys: adds additional name lookup keys + * - Chinese character's pinyin and pinyin's initial character. + * - Latin word and initial character. + * labels: unchanged + * Simplified Chinese labels are the same as English: [A-Z], #, " " + */ + private static class SimplifiedChineseContactUtils + extends LocaleUtilsBase { + public SimplifiedChineseContactUtils(LocaleSet locales) { + super(locales); + } + + @Override + public Iterator<String> getNameLookupKeys(String name, int nameStyle) { + if (nameStyle != FullNameStyle.JAPANESE && + nameStyle != FullNameStyle.KOREAN) { + return getPinyinNameLookupKeys(name); + } + return null; + } + + public static Iterator<String> getPinyinNameLookupKeys(String name) { + // TODO : Reduce the object allocation. + HashSet<String> keys = new HashSet<String>(); + ArrayList<Token> tokens = HanziToPinyin.getInstance().getTokens(name); + final int tokenCount = tokens.size(); + final StringBuilder keyPinyin = new StringBuilder(); + final StringBuilder keyInitial = new StringBuilder(); + // There is no space among the Chinese Characters, the variant name + // lookup key wouldn't work for Chinese. The keyOriginal is used to + // build the lookup keys for itself. + final StringBuilder keyOriginal = new StringBuilder(); + for (int i = tokenCount - 1; i >= 0; i--) { + final Token token = tokens.get(i); + if (Token.UNKNOWN == token.type) { + continue; + } + if (Token.PINYIN == token.type) { + keyPinyin.insert(0, token.target); + keyInitial.insert(0, token.target.charAt(0)); + } else if (Token.LATIN == token.type) { + // Avoid adding space at the end of String. + if (keyPinyin.length() > 0) { + keyPinyin.insert(0, ' '); + } + if (keyOriginal.length() > 0) { + keyOriginal.insert(0, ' '); + } + keyPinyin.insert(0, token.source); + keyInitial.insert(0, token.source.charAt(0)); + } + keyOriginal.insert(0, token.source); + keys.add(keyOriginal.toString()); + keys.add(keyPinyin.toString()); + keys.add(keyInitial.toString()); + } + return keys.iterator(); + } + } + + private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase(); + private static LocaleUtils sSingleton; + + private final LocaleSet mLocales; + private final LocaleUtilsBase mUtils; + + private LocaleUtils(LocaleSet locales) { + if (locales == null) { + mLocales = LocaleSet.getDefault(); + } else { + mLocales = locales; + } + if (mLocales.isPrimaryLanguage(JAPANESE_LANGUAGE)) { + mUtils = new JapaneseContactUtils(mLocales); + } else if (mLocales.isPrimaryLocaleSimplifiedChinese()) { + mUtils = new SimplifiedChineseContactUtils(mLocales); + } else { + mUtils = new LocaleUtilsBase(mLocales); + } + Log.i(TAG, "AddressBook Labels [" + mLocales.toString() + "]: " + + getLabels().toString()); + } + + public boolean isLocale(LocaleSet locales) { + return mLocales.equals(locales); + } + + public static synchronized LocaleUtils getInstance() { + if (sSingleton == null) { + sSingleton = new LocaleUtils(LocaleSet.getDefault()); + } + return sSingleton; + } + + @VisibleForTesting + public static synchronized void setLocale(Locale locale) { + setLocales(new LocaleSet(locale)); + } + + public static synchronized void setLocales(LocaleSet locales) { + if (sSingleton == null || !sSingleton.isLocale(locales)) { + sSingleton = new LocaleUtils(locales); + } + } + + public String getSortKey(String name, int nameStyle) { + return mUtils.getSortKey(name); + } + + public int getBucketIndex(String name) { + return mUtils.getBucketIndex(name); + } + + public int getBucketCount() { + return mUtils.getBucketCount(); + } + + public String getBucketLabel(int bucketIndex) { + return mUtils.getBucketLabel(bucketIndex); + } + + public String getLabel(String name) { + return getBucketLabel(getBucketIndex(name)); + } + + public ArrayList<String> getLabels() { + return mUtils.getLabels(); + } +} diff --git a/src/com/android/launcher3/settings/SettingsProvider.java b/src/com/android/launcher3/settings/SettingsProvider.java index 0bcdc69ae..2f36fc422 100644 --- a/src/com/android/launcher3/settings/SettingsProvider.java +++ b/src/com/android/launcher3/settings/SettingsProvider.java @@ -44,6 +44,7 @@ public final class SettingsProvider { public static final String SETTINGS_UI_GENERAL_ICONS_TEXT_FONT_FAMILY = "ui_general_icons_text_font"; public static final String SETTINGS_UI_GENERAL_ICONS_TEXT_FONT_STYLE = "ui_general_icons_text_font_style"; public static final String SETTINGS_UI_DRAWER_SORT_MODE = "ui_drawer_sort_mode"; + public static final String SETTINGS_UI_DRAWER_TYPE = "ui_drawer_type"; public static SharedPreferences get(Context context) { return context.getSharedPreferences(SETTINGS_KEY, Context.MODE_MULTI_PROCESS); |