summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJorge Ruesga <jorge@ruesga.com>2015-08-12 01:05:08 +0200
committerAdnan Begovic <adnan@cyngn.com>2015-12-01 14:09:26 -0800
commit8d17b9a2f33925c6678b1718dae29abfc7e2c989 (patch)
tree29ed65233460a30179d8fcb59ac67408bf7b58f6
parentc9503e0919ddeae38da404fbec6772bba52e0626 (diff)
downloadpackages_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-xAndroidManifest.xml16
-rw-r--r--assets/contributors.dbbin0 -> 263168 bytes
-rw-r--r--res/drawable/ic_person.xml27
-rw-r--r--res/drawable/ic_warning.xml27
-rw-r--r--res/layout/contributors_search_result.xml34
-rw-r--r--res/layout/contributors_view.xml80
-rw-r--r--res/menu/contributors_menu.xml32
-rw-r--r--res/values/cm_colors.xml6
-rw-r--r--res/values/cm_strings.xml15
-rw-r--r--res/xml/device_info_settings.xml8
-rw-r--r--src/com/android/settings/Settings.java3
-rw-r--r--src/com/android/settings/SettingsActivity.java7
-rw-r--r--src/com/android/settings/contributors/ContributorsCloudFragment.java774
-rw-r--r--src/com/android/settings/contributors/ContributorsCloudViewController.java1304
-rw-r--r--src/com/android/settings/cyanogenmod/BootReceiver.java4
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
new file mode 100644
index 0000000..4bf4560
--- /dev/null
+++ b/assets/contributors.db
Binary files differ
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);
}
}