diff options
author | Jorge Ruesga <jorge@ruesga.com> | 2015-08-12 01:05:08 +0200 |
---|---|---|
committer | Adnan Begovic <adnan@cyngn.com> | 2015-12-01 14:09:26 -0800 |
commit | 8d17b9a2f33925c6678b1718dae29abfc7e2c989 (patch) | |
tree | 29ed65233460a30179d8fcb59ac67408bf7b58f6 | |
parent | c9503e0919ddeae38da404fbec6772bba52e0626 (diff) | |
download | packages_apps_settings-8d17b9a2f33925c6678b1718dae29abfc7e2c989.zip packages_apps_settings-8d17b9a2f33925c6678b1718dae29abfc7e2c989.tar.gz packages_apps_settings-8d17b9a2f33925c6678b1718dae29abfc7e2c989.tar.bz2 |
settings: contributors cloud
Squash of:
settings: remove contributors cloud margin/padding
Change-Id: I120e7bd1611bd47126d91ca1f88ce5cdb964fed8
Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
Settings: update contributors cloud
Generated date: 3rd November 2015: 09:29
Change-Id: I7bc707f3896f91b3fd7d0f9f76700e3938f1a4b2
Change-Id: If5b89e0d278b7a0c85c966e09264b60927889fc9
JIRA: CML-133
Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
-rwxr-xr-x | AndroidManifest.xml | 16 | ||||
-rw-r--r-- | assets/contributors.db | bin | 0 -> 263168 bytes | |||
-rw-r--r-- | res/drawable/ic_person.xml | 27 | ||||
-rw-r--r-- | res/drawable/ic_warning.xml | 27 | ||||
-rw-r--r-- | res/layout/contributors_search_result.xml | 34 | ||||
-rw-r--r-- | res/layout/contributors_view.xml | 80 | ||||
-rw-r--r-- | res/menu/contributors_menu.xml | 32 | ||||
-rw-r--r-- | res/values/cm_colors.xml | 6 | ||||
-rw-r--r-- | res/values/cm_strings.xml | 15 | ||||
-rw-r--r-- | res/xml/device_info_settings.xml | 8 | ||||
-rw-r--r-- | src/com/android/settings/Settings.java | 3 | ||||
-rw-r--r-- | src/com/android/settings/SettingsActivity.java | 7 | ||||
-rw-r--r-- | src/com/android/settings/contributors/ContributorsCloudFragment.java | 774 | ||||
-rw-r--r-- | src/com/android/settings/contributors/ContributorsCloudViewController.java | 1304 | ||||
-rw-r--r-- | src/com/android/settings/cyanogenmod/BootReceiver.java | 4 |
15 files changed, 2330 insertions, 7 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2800a40..43ebbd1 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2272,6 +2272,20 @@ android:resource="@id/security_settings" /> </activity-alias> + <activity android:name="Settings$ContributorsCloudActivity" + android:label="@string/contributors_cloud_fragment_title" + android:windowSoftInputMode="stateHidden|adjustNothing" + android:taskAffinity="" + android:excludeFromRecents="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <action android:name="android.settings.CONTRIBUTORS" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + <meta-data android:name="com.android.settings.FRAGMENT_CLASS" + android:value="com.android.settings.contributors.ContributorsCloudFragment" /> + </activity> + <!-- CyanogenMod activities End --> <!-- Pseudo-activity used to provide an intent-filter entry point to encryption settings --> @@ -2813,4 +2827,4 @@ </activity> </application> -</manifest> +</manifest>
\ No newline at end of file diff --git a/assets/contributors.db b/assets/contributors.db Binary files differnew file mode 100644 index 0000000..4bf4560 --- /dev/null +++ b/assets/contributors.db diff --git a/res/drawable/ic_person.xml b/res/drawable/ic_person.xml new file mode 100644 index 0000000..9a211b4 --- /dev/null +++ b/res/drawable/ic_person.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="@color/theme_accent" + android:pathData="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 +4v2h16v-2c0-2.66-5.33-4-8-4z" /> +</vector> diff --git a/res/drawable/ic_warning.xml b/res/drawable/ic_warning.xml new file mode 100644 index 0000000..4001b74 --- /dev/null +++ b/res/drawable/ic_warning.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="512dp" + android:height="442.182dp" + android:viewportWidth="512" + android:viewportHeight="442.182"> + + <path + android:fillColor="#000" + android:pathData="M0,442.182h512L256,0L0,442.182z M279.272,372.363h-46.545v-46.545h46.545V372.363z +M279.272,279.272h-46.545v-93.091 h46.545V279.272z" /> +</vector> diff --git a/res/layout/contributors_search_result.xml b/res/layout/contributors_search_result.xml new file mode 100644 index 0000000..c5607da --- /dev/null +++ b/res/layout/contributors_search_result.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:orientation="vertical" + android:layout_margin="16dp" + android:gravity="center_vertical"> + + <TextView android:id="@+id/contributor_name" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:drawableStart="@drawable/ic_person" + android:drawablePadding="8dp" + android:singleLine="true" + android:maxLines="1" + android:ellipsize="end" + android:gravity="center_vertical" /> +</LinearLayout> diff --git a/res/layout/contributors_view.xml b/res/layout/contributors_view.xml new file mode 100644 index 0000000..1c6a1ee --- /dev/null +++ b/res/layout/contributors_view.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ImageView + android:id="@+id/contributors_cloud_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/contributors_cloud_fragment_title" + android:visibility="gone"/> + + <LinearLayout + android:id="@+id/contributors_cloud_loading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_gravity="center" + android:visibility="visible"> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:indeterminateOnly="true" + android:layout_gravity="center_horizontal"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_gravity="center_horizontal" + android:text="@string/contributors_cloud_loading_message"> + </TextView> + </LinearLayout> + + <LinearLayout + android:id="@+id/contributors_cloud_failed" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_gravity="center" + android:visibility="gone"> + + <ImageView + android:layout_width="64dp" + android:layout_height="64dp" + android:src="@drawable/ic_warning" + android:contentDescription="@string/contributors_cloud_failed_message" + android:layout_gravity="center_horizontal"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_gravity="center_horizontal" + android:text="@string/contributors_cloud_failed_message"> + </TextView> + </LinearLayout> + + <ListView + android:id="@+id/contributors_cloud_search_results" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" /> +</FrameLayout> diff --git a/res/menu/contributors_menu.xml b/res/menu/contributors_menu.xml new file mode 100644 index 0000000..d6473a1 --- /dev/null +++ b/res/menu/contributors_menu.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/contributors_search" + android:title="@string/search_menu" + android:icon="@*android:drawable/ic_search_api_material" + android:showAsAction="collapseActionView|ifRoom" + android:actionViewClass="android.widget.SearchView" /> + <item + android:id="@+id/contributor_info" + android:title="@string/contributor_info_menu" + android:showAsAction="never" /> + <item + android:id="@+id/contributions_info" + android:title="@string/contributions_info_menu" + android:showAsAction="never" /> +</menu> diff --git a/res/values/cm_colors.xml b/res/values/cm_colors.xml index e4ba40a..751108c 100644 --- a/res/values/cm_colors.xml +++ b/res/values/cm_colors.xml @@ -76,4 +76,8 @@ limitations under the License. <!-- Storage Summary Hard colors--> <color name="storage_summary_text_color">#ff607d8b</color> <color name="storage_summary_used_text_color">#8a000000</color> -</resources> + + <!-- Contributors --> + <color name="contributors_cloud_fg_color">@color/theme_accent</color> + <color name="contributors_cloud_selected_color">#ff5252</color> +</resources>
\ No newline at end of file diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml index f28615f..eb68f1a 100644 --- a/res/values/cm_strings.xml +++ b/res/values/cm_strings.xml @@ -827,4 +827,19 @@ <string name="pa_login_checking_password">Checking account\u2026</string> <string name="pa_login_incorrect_login">Login was incorrect</string> + <!-- Contributors cloud activity --> + <string name="contributors_cloud_fragment_title">Contributors</string> + <string name="contributors_cloud_loading_message">Loading contributors data\u2026</string> + <string name="contributors_cloud_failed_message">Cannot load contributors data</string> + <string name="contributor_info_menu">Contributor info</string> + <string name="contributor_info_msg"> + <![CDATA[<b>Name:</b> <xliff:g id="name">%1$s</xliff:g><br/><br/> + <b>Nick:</b> <xliff:g id="nick">%2$s</xliff:g><br/><br/> + <b>Commits:</b> <xliff:g id="commits">%3$s</xliff:g><br/><br/> + <b>Rank:</b> <xliff:g id="rank">%4$s</xliff:g>]]></string> + <string name="contributions_info_menu">Contributions info</string> + <string name="contributions_info_msg"> + <![CDATA[<b>Total contributors:</b> <xliff:g id="total_contributors">%1$s</xliff:g><br/><br/> + <b>Total commits:</b> <xliff:g id="total_commits">%2$s</xliff:g><br/><br/> + <b>Last update:</b> <xliff:g id="date">%3$s</xliff:g>]]></string> </resources> diff --git a/res/xml/device_info_settings.xml b/res/xml/device_info_settings.xml index 776402c..6cda1ed 100644 --- a/res/xml/device_info_settings.xml +++ b/res/xml/device_info_settings.xml @@ -83,6 +83,12 @@ android:title="@string/device_feedback"> </PreferenceScreen> + <!-- Contributors cloud --> + <PreferenceScreen android:key="contributors_cloud" + android:title="@string/contributors_cloud_fragment_title" + android:fragment="com.android.settings.contributors.ContributorsCloudFragment" > + </PreferenceScreen> + <!-- Device hardware model --> <Preference android:key="device_model" style="?android:preferenceInformationStyle" @@ -149,4 +155,4 @@ android:title="@string/selinux_status" android:summary="@string/selinux_status_enforcing"/> -</PreferenceScreen> +</PreferenceScreen>
\ No newline at end of file diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index 839b299..380d24f 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -122,4 +122,5 @@ public class Settings extends SettingsActivity { public static class DisplayRotationActivity extends SettingsActivity { /* empty */ } public static class BlacklistSettingsActivity extends SettingsActivity { /* empty */ } public static class AnonymousStatsActivity extends Settings { /* empty */ } -} + public static class ContributorsCloudActivity extends SettingsActivity { /* empty */ } +}
\ No newline at end of file diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index 5dcff22..9dc5021 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -81,6 +81,7 @@ import com.android.settings.applications.UsageAccessDetails; import com.android.settings.applications.WriteSettingsDetails; import com.android.settings.blacklist.BlacklistSettings; import com.android.settings.bluetooth.BluetoothSettings; +import com.android.settings.contributors.ContributorsCloudFragment; import com.android.settings.cyanogenmod.DisplayRotation; import com.android.settings.dashboard.DashboardCategory; import com.android.settings.dashboard.DashboardSummary; @@ -367,7 +368,8 @@ public class SettingsActivity extends Activity LiveDisplay.class.getName(), com.android.settings.cyanogenmod.DisplayRotation.class.getName(), com.android.settings.cyanogenmod.PrivacySettings.class.getName(), - BlacklistSettings.class.getName() + BlacklistSettings.class.getName(), + ContributorsCloudFragment.class.getName() }; @@ -1561,5 +1563,4 @@ public class SettingsActivity extends Activity super.onNewIntent(intent); } -} - +}
\ No newline at end of file diff --git a/src/com/android/settings/contributors/ContributorsCloudFragment.java b/src/com/android/settings/contributors/ContributorsCloudFragment.java new file mode 100644 index 0000000..e600e8d --- /dev/null +++ b/src/com/android/settings/contributors/ContributorsCloudFragment.java @@ -0,0 +1,774 @@ +/* + * 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.settings.contributors; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.text.Html; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.LinearInterpolator; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SearchView; +import android.widget.TextView; +import android.widget.AdapterView.OnItemClickListener; + +import com.android.settings.R; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class ContributorsCloudFragment extends Fragment implements SearchView.OnQueryTextListener, + SearchView.OnCloseListener, MenuItem.OnActionExpandListener { + + private static final String TAG = "ContributorsCloud"; + + private static final String DB_NAME = "contributors.db"; + + private static final String STATE_SELECTED_CONTRIBUTOR = "state_selected_contributor"; + + private ContributorsCloudViewController mViewController; + private ImageView mImageView; + private View mLoadingView; + private View mFailedView; + private ListView mSearchResults; + private ContributorsAdapter mSearchAdapter; + + private SQLiteDatabase mDatabase; + + private int mTotalContributors; + private int mTotalCommits; + private long mLastUpdate; + + private int mSelectedContributor = -1; + private String mContributorName; + private String mContributorNick; + private int mContributorCommits; + private int mContributorRank; + + private MenuItem mSearchMenuItem; + private MenuItem mContributorInfoMenuItem; + private MenuItem mContributionsInfoMenuItem; + private SearchView mSearchView; + + private Handler mHandler; + + private static class ViewInfo { + Bitmap mBitmap; + float mFocusX; + float mFocusY; + } + + private static class ContributorsDataHolder { + int mId; + String mLabel; + } + + private static class ContributorsViewHolder { + TextView mLabel; + } + + private static class ContributorsAdapter extends ArrayAdapter<ContributorsDataHolder> { + + public ContributorsAdapter(Context context) { + super(context, R.id.contributor_name, new ArrayList<ContributorsDataHolder>()); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + LayoutInflater li = LayoutInflater.from(getContext()); + convertView = li.inflate(R.layout.contributors_search_result, null); + ContributorsViewHolder viewHolder = new ContributorsViewHolder(); + viewHolder.mLabel = (TextView) convertView.findViewById(R.id.contributor_name); + convertView.setTag(viewHolder); + } + + ContributorsDataHolder dataHolder = getItem(position); + + ContributorsViewHolder viewHolder = (ContributorsViewHolder) convertView.getTag(); + viewHolder.mLabel.setText(dataHolder.mLabel); + + return convertView; + } + + @Override + public boolean hasStableIds() { + return true; + } + } + + private class ContributorCloudLoaderTask extends AsyncTask<Void, Void, Boolean> { + private ViewInfo mViewInfo; + private final boolean mNotify; + private final boolean mNavigate; + + public ContributorCloudLoaderTask(boolean notify, boolean navigate) { + mNotify = notify; + mNavigate = navigate; + } + + @Override + protected void onPreExecute() { + mLoadingView.setAlpha(1f); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + loadContributorsInfo(getActivity()); + loadUserInfo(getActivity()); + mViewInfo = generateViewInfo(getActivity(), mSelectedContributor); + if (mViewInfo != null && mViewInfo.mBitmap != null) { + return Boolean.TRUE; + } + + } catch (Exception ex) { + Log.e(TAG, "Failed to generate cloud bitmap", ex); + } + return Boolean.FALSE; + } + + @Override + protected void onPostExecute(Boolean result) { + if (result == true) { + mImageView.setImageBitmap(mViewInfo.mBitmap); + mViewController.update(); + if (mNotify) { + if (mNavigate) { + onLoadCloudDataSuccess(mViewInfo.mFocusX, mViewInfo.mFocusY); + } else { + onLoadCloudDataSuccess(-1, -1); + } + } + } else { + mImageView.setImageBitmap(null); + mViewController.update(); + if (mViewInfo != null && mViewInfo.mBitmap != null) { + mViewInfo.mBitmap.recycle(); + } + if (mNotify) { + onLoadCloudDataFailed(); + } + } + } + + @Override + protected void onCancelled() { + onLoadCloudDataFailed(); + } + } + + public ContributorsCloudFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + + if (savedInstanceState != null) { + mSelectedContributor = savedInstanceState.getInt(STATE_SELECTED_CONTRIBUTOR, -1); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mDatabase != null && mDatabase.isOpen()) { + try { + mDatabase.close(); + } catch (SQLException ex) { + // Ignore + } + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(STATE_SELECTED_CONTRIBUTOR, mSelectedContributor); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + activity.getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); + mHandler = new Handler(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + // Remove all previous menus + int count = menu.size(); + for (int i = 0; i < count; i++) { + menu.removeItem(menu.getItem(i).getItemId()); + } + + inflater.inflate(R.menu.contributors_menu, menu); + + mSearchMenuItem = menu.findItem(R.id.contributors_search); + mContributorInfoMenuItem = menu.findItem(R.id.contributor_info); + mContributionsInfoMenuItem = menu.findItem(R.id.contributions_info); + mSearchView = (SearchView) mSearchMenuItem.getActionView(); + mSearchMenuItem.setOnActionExpandListener(this); + mSearchView.setOnQueryTextListener(this); + mSearchView.setOnCloseListener(this); + + showMenuItems(false); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.contributors_search: + mSearchView.setQuery("", false); + mSelectedContributor = -1; + + // Load the data from the database and fill the image + ContributorCloudLoaderTask task = new ContributorCloudLoaderTask(false, false); + task.execute(); + break; + + case R.id.contributor_info: + showUserInfo(getActivity()); + break; + + case R.id.contributions_info: + showContributorsInfo(getActivity()); + break; + + default: + break; + } + return super.onContextItemSelected(item); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) { + View v = inflater.inflate(R.layout.contributors_view, container, false); + + mLoadingView= v.findViewById(R.id.contributors_cloud_loading); + mFailedView= v.findViewById(R.id.contributors_cloud_failed); + mImageView = (ImageView) v.findViewById(R.id.contributors_cloud_image); + mViewController = new ContributorsCloudViewController(mImageView); + mViewController.setMaximumScale(20f); + mViewController.setMediumScale(7f); + + mSearchResults = (ListView) v.findViewById(R.id.contributors_cloud_search_results); + mSearchAdapter = new ContributorsAdapter(getActivity()); + mSearchResults.setAdapter(mSearchAdapter); + mSearchResults.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + ContributorsDataHolder contributor = + (ContributorsDataHolder) parent.getItemAtPosition(position); + onContributorSelected(contributor); + } + }); + + // Load the data from the database and fill the image + ContributorCloudLoaderTask task = new ContributorCloudLoaderTask(true, false); + task.execute(); + + return v; + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + if (item.getItemId() == mSearchMenuItem.getItemId()) { + animateFadeOutFadeIn(mImageView, mSearchResults); + mContributorInfoMenuItem.setVisible(false); + mContributionsInfoMenuItem.setVisible(false); + } + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + if (item.getItemId() == mSearchMenuItem.getItemId()) { + animateFadeOutFadeIn(mSearchResults, mImageView); + if (mSelectedContributor != -1) { + mContributorInfoMenuItem.setVisible(true); + } + mContributionsInfoMenuItem.setVisible(true); + } + return true; + } + + @Override + public boolean onClose() { + animateFadeOutFadeIn(mSearchResults, mImageView); + return true; + } + + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + List<ContributorsDataHolder> contributors = new ArrayList<>(); + if (!TextUtils.isEmpty(newText) || newText.length() >= 3) { + contributors.addAll(performFilter(getActivity(), newText)); + } + mSearchAdapter.clear(); + mSearchAdapter.addAll(contributors); + mSearchAdapter.notifyDataSetChanged(); + return true; + } + + private void showMenuItems(boolean visible) { + mSearchMenuItem.setVisible(visible); + mContributorInfoMenuItem.setVisible(mSelectedContributor != -1 && visible); + mContributionsInfoMenuItem.setVisible(visible); + if (!visible) { + mSearchView.setQuery("", false); + mSearchMenuItem.collapseActionView(); + } + } + + private void onLoadCloudDataSuccess(float focusX, float focusY) { + animateFadeOutFadeIn(mLoadingView.getVisibility() == View.VISIBLE + ? mLoadingView : mSearchResults, mImageView); + showMenuItems(true); + + // Navigate to contributor? + if (focusX != -1 && focusY != -1) { + mViewController.setZoomTransitionDuration(2500); + mViewController.setScale(10, focusX, focusY, true); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mViewController.setZoomTransitionDuration(-1); + } + }, 2500); + } + } + + private void onLoadCloudDataFailed() { + // Show the cloud not loaded message + animateFadeOutFadeIn(mLoadingView.getVisibility() == View.VISIBLE + ? mLoadingView : (mImageView.getVisibility() == View.VISIBLE) + ? mImageView : mSearchResults, mFailedView); + showMenuItems(false); + } + + private void animateFadeOutFadeIn(final View src, final View dst) { + if (dst.getVisibility() != View.VISIBLE || dst.getAlpha() != 1f) { + AnimatorSet set = new AnimatorSet(); + set.playSequentially( + ObjectAnimator.ofFloat(src, "alpha", 0f), + ObjectAnimator.ofFloat(dst, "alpha", 1f)); + set.setInterpolator(new LinearInterpolator()); + set.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + src.setAlpha(1f); + dst.setAlpha(0f); + src.setVisibility(View.VISIBLE); + dst.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + src.setVisibility(View.GONE); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + }); + set.setDuration(250); + set.start(); + } else { + src.setAlpha(1f); + src.setVisibility(View.GONE); + } + } + + private ViewInfo generateViewInfo(Context context, int selectedId) { + Bitmap bitmap = null; + float focusX = -1, focusY = -1; + final Resources res = context.getResources(); + + // Open the database + SQLiteDatabase db = getDatabase(context, true); + if (db == null) { + // We don't have a valid database reference + return null; + } + + // Extract original image size + Cursor c = db.rawQuery("select value from info where key = ?;", new String[]{"orig_size"}); + if (c == null || !c.moveToFirst()) { + // We don't have a valid cursor reference + return null; + } + int osize = c.getInt(0); + c.close(); + + // Query the metadata table to extract all the commits information + c = db.rawQuery("select id, name, x, y, r, fs from metadata;", null); + if (c == null) { + // We don't have a valid cursor reference + return null; + } + try { + int colorForeground = res.getColor(R.color.contributors_cloud_fg_color); + int colorSelected = res.getColor(R.color.contributors_cloud_selected_color); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + + // Create a bitmap large enough to hold the cloud (use large bitmap when available) + int bsize = hasLargeHeap() ? 2048 : 1024; + bitmap = Bitmap.createBitmap(bsize, bsize, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + // Draw every contributor name + while (c.moveToNext()) { + int id = c.getInt(c.getColumnIndexOrThrow("id")); + + String name = c.getString(c.getColumnIndexOrThrow("name")); + float x = translate(c.getFloat(c.getColumnIndexOrThrow("x")), osize, bsize); + float y = translate(c.getFloat(c.getColumnIndexOrThrow("y")), osize, bsize); + int r = c.getInt(c.getColumnIndexOrThrow("r")); + float fs = translate(c.getFloat(c.getColumnIndexOrThrow("fs")), osize, bsize); + if (id < 0) { + y -= translate(fs, osize, bsize); + } + + // Choose the correct paint + paint.setColor(selectedId == id ? colorSelected : colorForeground); + paint.setTextSize(fs); + + // Check text rotation + float w = 0f, h = 0f; + if (selectedId == id || r != 0) { + Rect bounds = new Rect(); + paint.getTextBounds(name, 0, name.length(), bounds); + h = bounds.height(); + } + if (selectedId == id || r == -1) { + w = paint.measureText(name); + } + if (r == 0) { + // Horizontal + canvas.drawText(name, x, y, paint); + } else { + if (r == -1) { + // Vertical (-90 rotation) + canvas.save(); + canvas.translate(h, w - h); + canvas.rotate(-90, x, y); + canvas.drawText(name, x, y, paint); + canvas.restore(); + } else { + // Vertical (+90 rotation) + canvas.save(); + canvas.translate(h/2, -h); + canvas.rotate(90, x, y); + canvas.drawText(name, x, y, paint); + canvas.restore(); + } + } + + // Calculate focus + if (selectedId == id) { + int iw = mImageView.getWidth(); + int ih = mImageView.getHeight(); + int cx = iw / 2; + int cy = ih / 2; + int cbx = bsize / 2; + int cby = bsize / 2; + float cw = 0f; + float ch = 0f; + if (r == 0) { + cw = translate(w, bsize, Math.min(iw, ih)) / 2; + ch = translate(h, bsize, Math.min(iw, ih)) / 2; + } else { + cw = translate(h, bsize, Math.min(iw, ih)) / 2; + ch = translate(w, bsize, Math.min(iw, ih)) / 2; + } + + focusX = cx + translate(x - cbx, bsize, iw) + cw; + focusY = cy + translate(y - cby, bsize, ih) + ch; + } + } + + } finally { + c.close(); + } + + // Return the bitmap + ViewInfo viewInfo = new ViewInfo(); + viewInfo.mBitmap = bitmap; + viewInfo.mFocusX = focusX; + viewInfo.mFocusY = focusY; + return viewInfo; + } + + private synchronized SQLiteDatabase getDatabase(Context context, boolean retryCopyIfOpenFails) { + if (mDatabase == null) { + File dbPath = context.getDatabasePath(DB_NAME); + try { + mDatabase = SQLiteDatabase.openDatabase(dbPath.getAbsolutePath(), + null, SQLiteDatabase.OPEN_READONLY); + if (mDatabase == null) { + Log.e(TAG, "Cannot open cloud database: " + DB_NAME + ". db == null"); + return null; + } + return mDatabase; + + } catch (SQLException ex) { + Log.e(TAG, "Cannot open cloud database: " + DB_NAME, ex); + if (mDatabase != null && mDatabase.isOpen()) { + try { + mDatabase.close(); + } catch (SQLException ex2) { + // Ignore + } + } + + if (retryCopyIfOpenFails) { + extractContributorsCloudDatabase(context); + mDatabase = getDatabase(context, false); + } + } + + // We don't have a valid connection + return null; + } + return mDatabase; + } + + private void loadContributorsInfo(Context context) { + mTotalContributors = -1; + mTotalCommits = -1; + mLastUpdate = -1; + + // Open the database + SQLiteDatabase db = getDatabase(context, true); + if (db == null) { + // We don't have a valid database reference + return; + } + + // Total contributors + Cursor c = db.rawQuery("select count(*) from metadata where id > 0;", null); + if (c == null || !c.moveToFirst()) { + // We don't have a valid cursor reference + return; + } + mTotalContributors = c.getInt(0); + c.close(); + + // Total commits + c = db.rawQuery("select sum(commits) from metadata where id > 0;", null); + if (c == null || !c.moveToFirst()) { + // We don't have a valid cursor reference + return; + } + mTotalCommits = c.getInt(0); + c.close(); + + // Last update + c = db.rawQuery("select value from info where key = ?;", new String[]{"date"}); + if (c == null || !c.moveToFirst()) { + // We don't have a valid cursor reference + return; + } + mLastUpdate = c.getLong(0); + c.close(); + } + + private void loadUserInfo(Context context) { + // Open the database + SQLiteDatabase db = getDatabase(context, true); + if (db == null) { + // We don't have a valid database reference + return; + } + + // Total contributors + String[] args = new String[]{String.valueOf(mSelectedContributor)}; + Cursor c = db.rawQuery("select m1.name, m1.username, m1.commits," + + "(select count(*)+1 from metadata as m2 where " + + "m2.commits > m1.commits) as rank from metadata as m1 where m1.id = ?;", args); + if (c == null || !c.moveToFirst()) { + // We don't have a valid cursor reference + return; + } + mContributorName = c.getString(0); + mContributorNick = c.getString(1); + mContributorCommits = c.getInt(2); + mContributorRank = c.getInt(3); + } + + private void showUserInfo(Context context) { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.getDefault()); + String name = mContributorName != null ? mContributorName : "-"; + String nick = mContributorNick != null ? mContributorNick : "-"; + String commits = mContributorName != null ? nf.format(mContributorCommits) : "-"; + String rank = mContributorName != null ? "#" + String.valueOf(mContributorRank) : "-"; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.contributor_info_menu); + builder.setMessage(Html.fromHtml(getString(R.string.contributor_info_msg, + name, nick, commits, rank))); + builder.setPositiveButton(android.R.string.ok, null); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + private void showContributorsInfo(Context context) { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.getDefault()); + java.text.DateFormat df = DateFormat.getLongDateFormat(context); + java.text.DateFormat tf = DateFormat.getTimeFormat(context); + String totalContributors = mTotalContributors != -1 + ? nf.format(mTotalContributors) : "-"; + String totalCommits = mTotalCommits != -1 + ? nf.format(mTotalCommits) : "-"; + String lastUpdate = mLastUpdate != -1 + ? df.format(mLastUpdate) + " " + tf.format(mLastUpdate) : "-"; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.contributions_info_menu); + builder.setMessage(Html.fromHtml(getString(R.string.contributions_info_msg, + totalContributors, totalCommits, lastUpdate))); + builder.setPositiveButton(android.R.string.ok, null); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + private List<ContributorsDataHolder> performFilter(Context context, String query) { + // Open the database + SQLiteDatabase db = getDatabase(context, false); + if (db == null) { + // We don't have a valid database reference + return new ArrayList<>(); + } + + // Total contributors + String[] args = new String[]{String.valueOf(query.replaceAll("\\|", ""))}; + Cursor c = db.rawQuery( + "select id, name || case when username is null then '' else ' <'||username||'>' end contributor " + + "from metadata where lower(filter) like lower('%' || ? || '%') and id > 0 " + + "order by commits desc", args); + if (c == null) { + // We don't have a valid cursor reference + return new ArrayList<>(); + } + List<ContributorsDataHolder> results = new ArrayList<>(); + while (c.moveToNext()) { + ContributorsDataHolder result = new ContributorsDataHolder(); + result.mId = c.getInt(0); + result.mLabel = c.getString(1); + results.add(result); + } + return results; + } + + private void onContributorSelected(ContributorsDataHolder contributor) { + mSelectedContributor = contributor.mId; + ContributorCloudLoaderTask task = new ContributorCloudLoaderTask(true, true); + task.execute(); + mSearchMenuItem.collapseActionView(); + } + + private boolean hasLargeHeap() { + ActivityManager am = (ActivityManager) getActivity().getSystemService(Context.ACTIVITY_SERVICE); + return am.getMemoryClass() >= 96; + } + + private float translate(float v, int ssize, int dsize) { + return (v * dsize) / ssize; + } + + + public static void extractContributorsCloudDatabase(Context context) { + final int BUFFER = 1024; + InputStream is = null; + OutputStream os = null; + File databasePath = context.getDatabasePath(DB_NAME); + try { + databasePath.getParentFile().mkdir(); + is = context.getResources().getAssets().open(DB_NAME, AssetManager.ACCESS_BUFFER); + os = new FileOutputStream(databasePath); + int read = -1; + byte[] data = new byte[BUFFER]; + while ((read = is.read(data, 0, BUFFER)) != -1) { + os.write(data, 0, read); + } + } catch (IOException ex) { + Log.e(TAG, "Failed to extract contributors database"); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ex) { + // Ignore + } + } + } + } +} diff --git a/src/com/android/settings/contributors/ContributorsCloudViewController.java b/src/com/android/settings/contributors/ContributorsCloudViewController.java new file mode 100644 index 0000000..807d363 --- /dev/null +++ b/src/com/android/settings/contributors/ContributorsCloudViewController.java @@ -0,0 +1,1304 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * 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.settings.contributors; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Matrix.ScaleToFit; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ScaleGestureDetector.OnScaleGestureListener; +import android.view.View.OnLongClickListener; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.OverScroller; + +import java.lang.ref.WeakReference; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_UP; + +public class ContributorsCloudViewController implements View.OnTouchListener, + ViewTreeObserver.OnGlobalLayoutListener { + + private static final String LOG_TAG = "ContributorsCloud"; + + public static final float DEFAULT_MAX_SCALE = 3.0f; + public static final float DEFAULT_MID_SCALE = 1.75f; + public static final float DEFAULT_MIN_SCALE = 1.0f; + public static final int DEFAULT_ZOOM_DURATION = 200; + + // let debug flag be dynamic, but still Proguard can be used to remove from + // release builds + private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); + + static final Interpolator sInterpolator = new AccelerateDecelerateInterpolator(); + int ZOOM_DURATION = DEFAULT_ZOOM_DURATION; + + static final int EDGE_NONE = -1; + static final int EDGE_LEFT = 0; + static final int EDGE_RIGHT = 1; + static final int EDGE_BOTH = 2; + + private float mMinScale = DEFAULT_MIN_SCALE; + private float mMidScale = DEFAULT_MID_SCALE; + private float mMaxScale = DEFAULT_MAX_SCALE; + + private boolean mAllowParentInterceptOnEdge = true; + private boolean mBlockParentIntercept = false; + + private static final int INVALID_POINTER_ID = -1; + private int mActivePointerId = INVALID_POINTER_ID; + private int mActivePointerIndex = 0; + private float mLastTouchX; + private float mLastTouchY; + private final float mTouchSlop; + private final float mMinimumVelocity; + private VelocityTracker mVelocityTracker; + private boolean mIsDragging; + private boolean mIgnoreDoubleTapScale; + + private static void checkZoomLevels(float minZoom, float midZoom, + float maxZoom) { + if (minZoom >= midZoom) { + throw new IllegalArgumentException( + "MinZoom has to be less than MidZoom"); + } else if (midZoom >= maxZoom) { + throw new IllegalArgumentException( + "MidZoom has to be less than MaxZoom"); + } + } + + /** + * @return true if the ImageView exists, and it's Drawable existss + */ + private static boolean hasDrawable(ImageView imageView) { + return null != imageView && null != imageView.getDrawable(); + } + + /** + * @return true if the ScaleType is supported. + */ + private static boolean isSupportedScaleType(final ScaleType scaleType) { + if (null == scaleType) { + return false; + } + + switch (scaleType) { + case MATRIX: + throw new IllegalArgumentException(scaleType.name() + + " is not supported in PhotoView"); + + default: + return true; + } + } + + /** + * Set's the ImageView's ScaleType to Matrix. + */ + private static void setImageViewScaleTypeMatrix(ImageView imageView) { + /** + * PhotoView sets it's own ScaleType to Matrix, then diverts all calls + * setScaleType to this.setScaleType automatically. + */ + if (null != imageView /*&& !(imageView instanceof IPhotoView)*/) { + if (!ScaleType.MATRIX.equals(imageView.getScaleType())) { + imageView.setScaleType(ScaleType.MATRIX); + } + } + } + + public class DefaultOnDoubleTapListener implements GestureDetector.OnDoubleTapListener { + + private ContributorsCloudViewController controller; + + public DefaultOnDoubleTapListener(ContributorsCloudViewController controller) { + setController(controller); + } + + public void setController(ContributorsCloudViewController controller) { + this.controller = controller; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (controller == null) + return false; + + ImageView imageView = controller.getImageView(); + + if (null != controller.getOnPhotoTapListener()) { + final RectF displayRect = controller.getDisplayRect(); + + if (null != displayRect) { + final float x = e.getX(), y = e.getY(); + + // Check to see if the user tapped on the photo + if (displayRect.contains(x, y)) { + + float xResult = (x - displayRect.left) + / displayRect.width(); + float yResult = (y - displayRect.top) + / displayRect.height(); + + controller.getOnPhotoTapListener().onPhotoTap(imageView, xResult, yResult); + return true; + } + } + } + if (null != controller.getOnViewTapListener()) { + controller.getOnViewTapListener().onViewTap(imageView, e.getX(), e.getY()); + } + + return false; + } + + @Override + public boolean onDoubleTap(MotionEvent ev) { + if (controller == null) + return false; + try { + float scale = controller.getScale(); + float x = ev.getX(); + float y = ev.getY(); + + if (!mIgnoreDoubleTapScale && scale < controller.getMediumScale()) { + controller.setScale(controller.getMediumScale(), x, y, true); + } else if (!mIgnoreDoubleTapScale && scale >= controller.getMediumScale() + && scale < controller.getMaximumScale()) { + controller.setScale(controller.getMaximumScale(), x, y, true); + } else { + controller.setScale(controller.getMinimumScale(), x, y, true); + } + mIgnoreDoubleTapScale = false; + } catch (ArrayIndexOutOfBoundsException e) { + // Can sometimes happen when getX() and getY() is called + } + return true; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + // Wait for the confirmed onDoubleTap() instead + return false; + } + + } + + private WeakReference<ImageView> mImageView; + + // Gesture Detectors + private GestureDetector mGestureDetector; + private ScaleGestureDetector mScaleDragDetector; + + // These are set so we don't keep allocating them on the heap + private final Matrix mBaseMatrix = new Matrix(); + private final Matrix mDrawMatrix = new Matrix(); + private final Matrix mSuppMatrix = new Matrix(); + private final RectF mDisplayRect = new RectF(); + private final float[] mMatrixValues = new float[9]; + + // Listeners + private OnMatrixChangedListener mMatrixChangeListener; + private OnPhotoTapListener mPhotoTapListener; + private OnViewTapListener mViewTapListener; + private OnLongClickListener mLongClickListener; + private OnScaleChangeListener mScaleChangeListener; + + private int mIvTop, mIvRight, mIvBottom, mIvLeft; + private FlingRunnable mCurrentFlingRunnable; + private int mScrollEdge = EDGE_BOTH; + + private boolean mZoomEnabled; + private ScaleType mScaleType = ScaleType.FIT_CENTER; + + public ContributorsCloudViewController(ImageView imageView) { + this(imageView, true); + } + + public ContributorsCloudViewController(ImageView imageView, boolean zoomable) { + final ViewConfiguration configuration = ViewConfiguration.get(imageView.getContext()); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mTouchSlop = configuration.getScaledTouchSlop(); + + mImageView = new WeakReference<>(imageView); + + imageView.setDrawingCacheEnabled(true); + imageView.setOnTouchListener(this); + + ViewTreeObserver observer = imageView.getViewTreeObserver(); + if (null != observer) + observer.addOnGlobalLayoutListener(this); + + // Make sure we using MATRIX Scale Type + setImageViewScaleTypeMatrix(imageView); + + if (imageView.isInEditMode()) { + return; + } + + // Create Gesture Detectors... + OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scaleFactor = detector.getScaleFactor(); + + if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) + return false; + + ContributorsCloudViewController.this.onScale(scaleFactor, + detector.getFocusX(), detector.getFocusY()); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + // NO-OP + } + }; + mScaleDragDetector = new ScaleGestureDetector(imageView.getContext(), mScaleListener); + + mGestureDetector = new GestureDetector(imageView.getContext(), + new GestureDetector.SimpleOnGestureListener() { + + // forward long click listener + @Override + public void onLongPress(MotionEvent e) { + if (null != mLongClickListener) { + mLongClickListener.onLongClick(getImageView()); + } + } + }); + mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this)); + + // Finally, update the UI so that we're zoomable + setZoomable(zoomable); + } + + public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { + if (newOnDoubleTapListener != null) { + this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); + } else { + this.mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this)); + } + } + + public void setOnScaleChangeListener(OnScaleChangeListener onScaleChangeListener) { + this.mScaleChangeListener = onScaleChangeListener; + } + + public boolean canZoom() { + return mZoomEnabled; + } + + /** + * Clean-up the resources attached to this object. This needs to be called when the ImageView is + * no longer used. A good example is from {@link android.view.View#onDetachedFromWindow()} or + * from {@link android.app.Activity#onDestroy()}. This is automatically called if you are using + * {@link uk.co.senab.photoview.PhotoView}. + */ + @SuppressWarnings("deprecation") + public void cleanup() { + if (null == mImageView) { + return; // cleanup already done + } + + final ImageView imageView = mImageView.get(); + + if (null != imageView) { + // Remove this as a global layout listener + ViewTreeObserver observer = imageView.getViewTreeObserver(); + if (null != observer && observer.isAlive()) { + observer.removeGlobalOnLayoutListener(this); + } + + // Remove the ImageView's reference to this + imageView.setOnTouchListener(null); + + // make sure a pending fling runnable won't be run + cancelFling(); + } + + if (null != mGestureDetector) { + mGestureDetector.setOnDoubleTapListener(null); + } + + // Clear listeners too + mMatrixChangeListener = null; + mPhotoTapListener = null; + mViewTapListener = null; + + // Finally, clear ImageView + mImageView = null; + } + + public RectF getDisplayRect() { + checkMatrixBounds(); + return getDisplayRect(getDrawMatrix()); + } + + public boolean setDisplayMatrix(Matrix finalMatrix) { + if (finalMatrix == null) + throw new IllegalArgumentException("Matrix cannot be null"); + + ImageView imageView = getImageView(); + if (null == imageView) + return false; + + if (null == imageView.getDrawable()) + return false; + + mSuppMatrix.set(finalMatrix); + setImageViewMatrix(getDrawMatrix()); + checkMatrixBounds(); + + return true; + } + + public void setRotationTo(float degrees) { + mSuppMatrix.setRotate(degrees % 360); + checkAndDisplayMatrix(); + } + + public void setRotationBy(float degrees) { + mSuppMatrix.postRotate(degrees % 360); + checkAndDisplayMatrix(); + } + + public ImageView getImageView() { + ImageView imageView = null; + + if (null != mImageView) { + imageView = mImageView.get(); + } + + // If we don't have an ImageView, call cleanup() + if (null == imageView) { + cleanup(); + Log.i(LOG_TAG, "ImageView no longer exists. You should " + + "not use this reference any more."); + } + + return imageView; + } + + public float getMinimumScale() { + return mMinScale; + } + + public float getMediumScale() { + return mMidScale; + } + + public float getMaximumScale() { + return mMaxScale; + } + + public float getScale() { + return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + + (float) Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); + } + + public ScaleType getScaleType() { + return mScaleType; + } + + public void onDrag(float dx, float dy) { + if (mScaleDragDetector.isInProgress()) { + return; // Do not drag if we are already scaling + } + + if (DEBUG) { + Log.d(LOG_TAG, String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy)); + } + + ImageView imageView = getImageView(); + mSuppMatrix.postTranslate(dx, dy); + checkAndDisplayMatrix(); + + /** + * Here we decide whether to let the ImageView's parent to start taking + * over the touch event. + * + * First we check whether this function is enabled. We never want the + * parent to take over if we're scaling. We then check the edge we're + * on, and the direction of the scroll (i.e. if we're pulling against + * the edge, aka 'overscrolling', let the parent take over). + */ + ViewParent parent = imageView.getParent(); + if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isInProgress() + && !mBlockParentIntercept) { + if (mScrollEdge == EDGE_BOTH + || (mScrollEdge == EDGE_LEFT && dx >= 1f) + || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { + if (null != parent) + parent.requestDisallowInterceptTouchEvent(false); + } + } else { + if (null != parent) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + mIgnoreDoubleTapScale = false; + } + + public void onFling(float startX, float startY, float velocityX, float velocityY) { + if (DEBUG) { + Log.d(LOG_TAG, "onFling. sX: " + startX + " sY: " + startY + " Vx: " + + velocityX + " Vy: " + velocityY); + } + ImageView imageView = getImageView(); + mCurrentFlingRunnable = new FlingRunnable(imageView.getContext()); + mCurrentFlingRunnable.fling(getImageViewWidth(imageView), + getImageViewHeight(imageView), (int) velocityX, (int) velocityY); + imageView.post(mCurrentFlingRunnable); + mIgnoreDoubleTapScale = false; + } + + @Override + public void onGlobalLayout() { + ImageView imageView = getImageView(); + + if (null != imageView) { + if (mZoomEnabled) { + final int top = imageView.getTop(); + final int right = imageView.getRight(); + final int bottom = imageView.getBottom(); + final int left = imageView.getLeft(); + + /** + * We need to check whether the ImageView's bounds have changed. + * This would be easier if we targeted API 11+ as we could just use + * View.OnLayoutChangeListener. Instead we have to replicate the + * work, keeping track of the ImageView's bounds and then checking + * if the values change. + */ + if (top != mIvTop || bottom != mIvBottom || left != mIvLeft + || right != mIvRight) { + // Update our base matrix, as the bounds have changed + updateBaseMatrix(imageView.getDrawable()); + + // Update values as something has changed + mIvTop = top; + mIvRight = right; + mIvBottom = bottom; + mIvLeft = left; + } + } else { + updateBaseMatrix(imageView.getDrawable()); + } + } + } + + public void onScale(float scaleFactor, float focusX, float focusY) { + if (DEBUG) { + Log.d(LOG_TAG,String.format("onScale: scale: %.2f. fX: %.2f. fY: %.2f", + scaleFactor, focusX, focusY)); + } + + if (getScale() < mMaxScale || scaleFactor < 1f) { + if (null != mScaleChangeListener) { + mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); + } + mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); + checkAndDisplayMatrix(); + } + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent ev) { + boolean handled = false; + + if (mZoomEnabled && hasDrawable((ImageView) v)) { + ViewParent parent = v.getParent(); + switch (ev.getAction()) { + case ACTION_DOWN: + // First, disable the Parent from intercepting the touch + // event + if (null != parent) { + parent.requestDisallowInterceptTouchEvent(true); + } else { + Log.i(LOG_TAG, "onTouch getParent() returned null"); + } + + // If we're flinging, and the user presses down, cancel + // fling + cancelFling(); + break; + + case ACTION_CANCEL: + case ACTION_UP: + // If the user has zoomed less than min scale, zoom back + // to min scale + if (getScale() < mMinScale) { + RectF rect = getDisplayRect(); + if (null != rect) { + v.post(new AnimatedZoomRunnable(getScale(), mMinScale, + rect.centerX(), rect.centerY())); + handled = true; + } + } + break; + } + + // Try the Scale/Drag detector + if (null != mScaleDragDetector) { + boolean wasScaling = mScaleDragDetector.isInProgress(); + boolean wasDragging = mIsDragging; + + handled = onTouchEvent(ev); + + boolean didntScale = !wasScaling && !mScaleDragDetector.isInProgress(); + boolean didntDrag = !wasDragging && !mIsDragging; + + mBlockParentIntercept = didntScale && didntDrag; + } + + // Check to see if the user double tapped + if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) { + handled = true; + } + + } + + return handled; + } + + public void setAllowParentInterceptOnEdge(boolean allow) { + mAllowParentInterceptOnEdge = allow; + } + + public void setMinimumScale(float minimumScale) { + checkZoomLevels(minimumScale, mMidScale, mMaxScale); + mMinScale = minimumScale; + } + + public void setMediumScale(float mediumScale) { + checkZoomLevels(mMinScale, mediumScale, mMaxScale); + mMidScale = mediumScale; + } + + public void setMaximumScale(float maximumScale) { + checkZoomLevels(mMinScale, mMidScale, maximumScale); + mMaxScale = maximumScale; + } + + public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { + checkZoomLevels(minimumScale, mediumScale, maximumScale); + mMinScale = minimumScale; + mMidScale = mediumScale; + mMaxScale = maximumScale; + } + + public void setOnLongClickListener(OnLongClickListener listener) { + mLongClickListener = listener; + } + + public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { + mMatrixChangeListener = listener; + } + + public void setOnPhotoTapListener(OnPhotoTapListener listener) { + mPhotoTapListener = listener; + } + + public OnPhotoTapListener getOnPhotoTapListener() { + return mPhotoTapListener; + } + + public void setOnViewTapListener(OnViewTapListener listener) { + mViewTapListener = listener; + } + + public OnViewTapListener getOnViewTapListener() { + return mViewTapListener; + } + + public void setScale(float scale) { + setScale(scale, false); + } + + public void setScale(float scale, boolean animate) { + ImageView imageView = getImageView(); + + if (null != imageView) { + setScale(scale, + (imageView.getRight()) / 2, + (imageView.getBottom()) / 2, + animate); + } + } + + public void setScale(float scale, float focalX, float focalY, boolean animate) { + ImageView imageView = getImageView(); + + if (null != imageView) { + // Check to see if the scale is within bounds + if (scale < mMinScale || scale > mMaxScale) { + Log.i(LOG_TAG, "Scale must be within the range of minScale and maxScale"); + return; + } + + if (animate) { + imageView.post(new AnimatedZoomRunnable(getScale(), scale, + focalX, focalY)); + } else { + mSuppMatrix.setScale(scale, scale, focalX, focalY); + checkAndDisplayMatrix(); + } + + // This focuses to some point of the view, so treat it as a return point to + // minimum zoom + if (scale < getMediumScale()) { + mIgnoreDoubleTapScale = true; + } + } + } + + public void setScaleType(ScaleType scaleType) { + if (isSupportedScaleType(scaleType) && scaleType != mScaleType) { + mScaleType = scaleType; + + // Finally update + update(); + } + } + + public void setZoomable(boolean zoomable) { + mZoomEnabled = zoomable; + update(); + } + + public void update() { + ImageView imageView = getImageView(); + + if (null != imageView) { + if (mZoomEnabled) { + // Make sure we using MATRIX Scale Type + setImageViewScaleTypeMatrix(imageView); + + // Update the base matrix using the current drawable + updateBaseMatrix(imageView.getDrawable()); + } else { + // Reset the Matrix... + resetMatrix(); + } + } + } + + public Matrix getDisplayMatrix() { + return new Matrix(getDrawMatrix()); + } + + public Matrix getDrawMatrix() { + mDrawMatrix.set(mBaseMatrix); + mDrawMatrix.postConcat(mSuppMatrix); + return mDrawMatrix; + } + + private void cancelFling() { + if (null != mCurrentFlingRunnable) { + mCurrentFlingRunnable.cancelFling(); + mCurrentFlingRunnable = null; + } + } + + /** + * Helper method that simply checks the Matrix, and then displays the result + */ + private void checkAndDisplayMatrix() { + if (checkMatrixBounds()) { + setImageViewMatrix(getDrawMatrix()); + } + } + + private void checkImageViewScaleType() { + ImageView imageView = getImageView(); + + /** + * PhotoView's getScaleType() will just divert to this.getScaleType() so + * only call if we're not attached to a PhotoView. + */ + if (null != imageView/* && !(imageView instanceof IPhotoView)*/) { + if (!ScaleType.MATRIX.equals(imageView.getScaleType())) { + throw new IllegalStateException("The ImageView's ScaleType has been " + + "changed since attaching to this controller"); + } + } + } + + private boolean checkMatrixBounds() { + final ImageView imageView = getImageView(); + if (null == imageView) { + return false; + } + + final RectF rect = getDisplayRect(getDrawMatrix()); + if (null == rect) { + return false; + } + + final float height = rect.height(), width = rect.width(); + float deltaX = 0, deltaY = 0; + + final int viewHeight = getImageViewHeight(imageView); + if (height <= viewHeight) { + switch (mScaleType) { + case FIT_START: + deltaY = -rect.top; + break; + case FIT_END: + deltaY = viewHeight - height - rect.top; + break; + default: + deltaY = (viewHeight - height) / 2 - rect.top; + break; + } + } else if (rect.top > 0) { + deltaY = -rect.top; + } else if (rect.bottom < viewHeight) { + deltaY = viewHeight - rect.bottom; + } + + final int viewWidth = getImageViewWidth(imageView); + if (width <= viewWidth) { + switch (mScaleType) { + case FIT_START: + deltaX = -rect.left; + break; + case FIT_END: + deltaX = viewWidth - width - rect.left; + break; + default: + deltaX = (viewWidth - width) / 2 - rect.left; + break; + } + mScrollEdge = EDGE_BOTH; + } else if (rect.left > 0) { + mScrollEdge = EDGE_LEFT; + deltaX = -rect.left; + } else if (rect.right < viewWidth) { + deltaX = viewWidth - rect.right; + mScrollEdge = EDGE_RIGHT; + } else { + mScrollEdge = EDGE_NONE; + } + + // Finally actually translate the matrix + mSuppMatrix.postTranslate(deltaX, deltaY); + return true; + } + + /** + * Helper method that maps the supplied Matrix to the current Drawable + * + * @param matrix - Matrix to map Drawable against + * @return RectF - Displayed Rectangle + */ + private RectF getDisplayRect(Matrix matrix) { + ImageView imageView = getImageView(); + + if (null != imageView) { + Drawable d = imageView.getDrawable(); + if (null != d) { + mDisplayRect.set(0, 0, d.getIntrinsicWidth(), + d.getIntrinsicHeight()); + matrix.mapRect(mDisplayRect); + return mDisplayRect; + } + } + return null; + } + + public Bitmap getVisibleRectangleBitmap() { + ImageView imageView = getImageView(); + return imageView == null ? null : imageView.getDrawingCache(); + } + + public void setZoomTransitionDuration(int milliseconds) { + if (milliseconds < 0) + milliseconds = DEFAULT_ZOOM_DURATION; + this.ZOOM_DURATION = milliseconds; + } + + /** + * Helper method that 'unpacks' a Matrix and returns the required value + * + * @param matrix - Matrix to unpack + * @param whichValue - Which value from Matrix.M* to return + * @return float - returned value + */ + private float getValue(Matrix matrix, int whichValue) { + matrix.getValues(mMatrixValues); + return mMatrixValues[whichValue]; + } + + /** + * Resets the Matrix back to FIT_CENTER, and then displays it.s + */ + private void resetMatrix() { + mSuppMatrix.reset(); + setImageViewMatrix(getDrawMatrix()); + checkMatrixBounds(); + } + + private void setImageViewMatrix(Matrix matrix) { + ImageView imageView = getImageView(); + if (null != imageView) { + + checkImageViewScaleType(); + imageView.setImageMatrix(matrix); + + // Call MatrixChangedListener if needed + if (null != mMatrixChangeListener) { + RectF displayRect = getDisplayRect(matrix); + if (null != displayRect) { + mMatrixChangeListener.onMatrixChanged(displayRect); + } + } + } + } + + /** + * Calculate Matrix for FIT_CENTER + * + * @param d - Drawable being displayed + */ + private void updateBaseMatrix(Drawable d) { + ImageView imageView = getImageView(); + if (null == imageView || null == d) { + return; + } + + final float viewWidth = getImageViewWidth(imageView); + final float viewHeight = getImageViewHeight(imageView); + final int drawableWidth = d.getIntrinsicWidth(); + final int drawableHeight = d.getIntrinsicHeight(); + + mBaseMatrix.reset(); + + final float widthScale = viewWidth / drawableWidth; + final float heightScale = viewHeight / drawableHeight; + + if (mScaleType == ScaleType.CENTER) { + mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, + (viewHeight - drawableHeight) / 2F); + + } else if (mScaleType == ScaleType.CENTER_CROP) { + float scale = Math.max(widthScale, heightScale); + mBaseMatrix.postScale(scale, scale); + mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, + (viewHeight - drawableHeight * scale) / 2F); + + } else if (mScaleType == ScaleType.CENTER_INSIDE) { + float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); + mBaseMatrix.postScale(scale, scale); + mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, + (viewHeight - drawableHeight * scale) / 2F); + + } else { + RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); + RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); + + switch (mScaleType) { + case FIT_CENTER: + mBaseMatrix + .setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); + break; + + case FIT_START: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); + break; + + case FIT_END: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); + break; + + case FIT_XY: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); + break; + + default: + break; + } + } + + resetMatrix(); + } + + private int getImageViewWidth(ImageView imageView) { + if (null == imageView) + return 0; + return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); + } + + private int getImageViewHeight(ImageView imageView) { + if (null == imageView) + return 0; + return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); + } + + private float getActiveX(MotionEvent ev) { + try { + return ev.getX(mActivePointerIndex); + } catch (Exception e) { + return ev.getX(); + } + } + + private float getActiveY(MotionEvent ev) { + try { + return ev.getY(mActivePointerIndex); + } catch (Exception e) { + return ev.getY(); + } + } + + public boolean onTouchEvent(MotionEvent ev) { + mScaleDragDetector.onTouchEvent(ev); + + final int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = ev.getPointerId(0); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mActivePointerId = INVALID_POINTER_ID; + break; + case MotionEvent.ACTION_POINTER_UP: + // Ignore deprecation, ACTION_POINTER_ID_MASK and + // ACTION_POINTER_ID_SHIFT has same value and are deprecated + // You can have either deprecation or lint target api warning + final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) + >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + mLastTouchX = ev.getX(newPointerIndex); + mLastTouchY = ev.getY(newPointerIndex); + } + break; + } + + mActivePointerIndex = ev.findPointerIndex( + mActivePointerId != INVALID_POINTER_ID ? mActivePointerId : 0); + + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: { + mVelocityTracker = VelocityTracker.obtain(); + if (null != mVelocityTracker) { + mVelocityTracker.addMovement(ev); + } else { + Log.i(LOG_TAG, "Velocity tracker is null"); + } + + mLastTouchX = getActiveX(ev); + mLastTouchY = getActiveY(ev); + mIsDragging = false; + break; + } + + case MotionEvent.ACTION_MOVE: { + final float x = getActiveX(ev); + final float y = getActiveY(ev); + final float dx = x - mLastTouchX, dy = y - mLastTouchY; + + if (!mIsDragging) { + // Use Pythagoras to see if drag length is larger than + // touch slop + mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; + } + + if (mIsDragging) { + onDrag(dx, dy); + mLastTouchX = x; + mLastTouchY = y; + + if (null != mVelocityTracker) { + mVelocityTracker.addMovement(ev); + } + } + break; + } + + case MotionEvent.ACTION_CANCEL: { + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + } + + case MotionEvent.ACTION_UP: { + if (mIsDragging) { + if (null != mVelocityTracker) { + mLastTouchX = getActiveX(ev); + mLastTouchY = getActiveY(ev); + + // Compute velocity within the last 1000ms + mVelocityTracker.addMovement(ev); + mVelocityTracker.computeCurrentVelocity(1000); + + final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker + .getYVelocity(); + + // If the velocity is greater than minVelocity, call + // listener + if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { + onFling(mLastTouchX, mLastTouchY, -vX, -vY); + } + } + } + + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + } + } + + return true; + } + + /** + * Interface definition for a callback to be invoked when the internal Matrix has changed for + * this View. + * + * @author Chris Banes + */ + public static interface OnMatrixChangedListener { + /** + * Callback for when the Matrix displaying the Drawable has changed. This could be because + * the View's bounds have changed, or the user has zoomed. + * + * @param rect - Rectangle displaying the Drawable's new bounds. + */ + void onMatrixChanged(RectF rect); + } + + /** + * Interface definition for callback to be invoked when attached ImageView scale changes + * + * @author Marek Sebera + */ + public static interface OnScaleChangeListener { + /** + * Callback for when the scale changes + * + * @param scaleFactor the scale factor (<1 for zoom out, >1 for zoom in) + * @param focusX focal point X position + * @param focusY focal point Y position + */ + void onScaleChange(float scaleFactor, float focusX, float focusY); + } + + /** + * Interface definition for a callback to be invoked when the Photo is tapped with a single + * tap. + * + * @author Chris Banes + */ + public static interface OnPhotoTapListener { + + /** + * A callback to receive where the user taps on a photo. You will only receive a callback if + * the user taps on the actual photo, tapping on 'whitespace' will be ignored. + * + * @param view - View the user tapped. + * @param x - where the user tapped from the of the Drawable, as percentage of the + * Drawable width. + * @param y - where the user tapped from the top of the Drawable, as percentage of the + * Drawable height. + */ + void onPhotoTap(View view, float x, float y); + } + + /** + * Interface definition for a callback to be invoked when the ImageView is tapped with a single + * tap. + * + * @author Chris Banes + */ + public static interface OnViewTapListener { + + /** + * A callback to receive where the user taps on a ImageView. You will receive a callback if + * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored. + * + * @param view - View the user tapped. + * @param x - where the user tapped from the left of the View. + * @param y - where the user tapped from the top of the View. + */ + void onViewTap(View view, float x, float y); + } + + private class AnimatedZoomRunnable implements Runnable { + + private final float mFocalX, mFocalY; + private final long mStartTime; + private final float mZoomStart, mZoomEnd; + + public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, + final float focalX, final float focalY) { + mFocalX = focalX; + mFocalY = focalY; + mStartTime = System.currentTimeMillis(); + mZoomStart = currentZoom; + mZoomEnd = targetZoom; + } + + @Override + public void run() { + ImageView imageView = getImageView(); + if (imageView == null) { + return; + } + + float t = interpolate(); + float scale = mZoomStart + t * (mZoomEnd - mZoomStart); + float deltaScale = scale / getScale(); + + onScale(deltaScale, mFocalX, mFocalY); + + // We haven't hit our target scale yet, so post ourselves again + if (t < 1f) { + imageView.postOnAnimation(this); + } + } + + private float interpolate() { + float t = 1f * (System.currentTimeMillis() - mStartTime) / ZOOM_DURATION; + t = Math.min(1f, t); + t = sInterpolator.getInterpolation(t); + return t; + } + } + + private class FlingRunnable implements Runnable { + + protected final OverScroller mScroller; + private int mCurrentX, mCurrentY; + + public FlingRunnable(Context context) { + mScroller = new OverScroller(context); + } + + public void cancelFling() { + if (DEBUG) { + Log.d(LOG_TAG, "Cancel Fling"); + } + mScroller.forceFinished(true); + } + + public void fling(int viewWidth, int viewHeight, int velocityX, + int velocityY) { + final RectF rect = getDisplayRect(); + if (null == rect) { + return; + } + + final int startX = Math.round(-rect.left); + final int minX, maxX, minY, maxY; + + if (viewWidth < rect.width()) { + minX = 0; + maxX = Math.round(rect.width() - viewWidth); + } else { + minX = maxX = startX; + } + + final int startY = Math.round(-rect.top); + if (viewHeight < rect.height()) { + minY = 0; + maxY = Math.round(rect.height() - viewHeight); + } else { + minY = maxY = startY; + } + + mCurrentX = startX; + mCurrentY = startY; + + if (DEBUG) { + Log.d(LOG_TAG, "fling. StartX:" + startX + " StartY:" + startY + + " MaxX:" + maxX + " MaxY:" + maxY); + } + + // If we actually can move, fling the scroller + if (startX != maxX || startY != maxY) { + mScroller.fling(startX, startY, velocityX, velocityY, minX, + maxX, minY, maxY, 0, 0); + } + } + + @Override + public void run() { + if (mScroller.isFinished()) { + return; // remaining post that should not be handled + } + + ImageView imageView = getImageView(); + if (null != imageView && mScroller.computeScrollOffset()) { + + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + + if (DEBUG) { + Log.d(LOG_TAG, "fling run(). CurrentX:" + mCurrentX + " CurrentY:" + + mCurrentY + " NewX:" + newX + " NewY:" + newY); + } + + mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); + setImageViewMatrix(getDrawMatrix()); + + mCurrentX = newX; + mCurrentY = newY; + + // Post On animation + imageView.postOnAnimation(this); + } + } + } +} diff --git a/src/com/android/settings/cyanogenmod/BootReceiver.java b/src/com/android/settings/cyanogenmod/BootReceiver.java index 761c8ec..19d2434 100644 --- a/src/com/android/settings/cyanogenmod/BootReceiver.java +++ b/src/com/android/settings/cyanogenmod/BootReceiver.java @@ -22,6 +22,7 @@ import android.content.Intent; import com.android.settings.ButtonSettings; import com.android.settings.R; import com.android.settings.Utils; +import com.android.settings.contributors.ContributorsCloudFragment; import com.android.settings.hardware.VibratorIntensity; import com.android.settings.inputmethod.InputMethodAndLanguageSettings; import com.android.settings.livedisplay.DisplayGamma; @@ -39,5 +40,8 @@ public class BootReceiver extends BroadcastReceiver { VibratorIntensity.restore(ctx); InputMethodAndLanguageSettings.restore(ctx); LocationSettings.restore(ctx); + + // Extract the contributors database + ContributorsCloudFragment.extractContributorsCloudDatabase(ctx); } } |