Merge "commons" into the project root directory

This commit is contained in:
Yusuke Matsubara 2016-07-02 16:20:43 +09:00
parent d42db0612e
commit b4231bbfdc
324 changed files with 22 additions and 23 deletions

View file

@ -0,0 +1,57 @@
package fr.free.nrw.commons.media;
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;
import org.mediawiki.api.ApiResult;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class CategoryImagesLoader extends AsyncTaskLoader<List<Media>>{
private final CommonsApplication app;
private final String category;
public CategoryImagesLoader(Context context, String category) {
super(context);
this.app = (CommonsApplication) context.getApplicationContext();
this.category = category;
}
@Override
protected void onStartLoading() {
super.onStartLoading();
super.forceLoad();
}
@Override
public List<Media> loadInBackground() {
ArrayList<Media> mediaList = new ArrayList<Media>();
ApiResult result;
try {
result = app.getApi().action("query")
.param("list", "categorymembers")
.param("cmtitle", "Category:" + category)
.param("cmprop", "title|timestamp")
.param("cmtype", "file")
.param("cmsort", "timestamp")
.param("cmdir", "descending")
.param("cmlimit", 50)
.get();
} catch (IOException e) {
throw new RuntimeException(e);
}
Log.d("Commons", Utils.getStringFromDOM(result.getDocument()));
List<ApiResult> members = result.getNodes("/api/query/categorymembers/cm");
for(ApiResult member : members) {
mediaList.add(new Media(member.getString("@title")));
}
return mediaList;
}
}

View file

@ -0,0 +1,367 @@
package fr.free.nrw.commons.media;
import android.content.Intent;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import com.android.volley.toolbox.ImageLoader;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.assist.ImageLoadingListener;
import java.io.IOException;
import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.License;
import fr.free.nrw.commons.LicenseList;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
public class MediaDetailFragment extends Fragment {
private boolean editable;
private DisplayImageOptions displayOptions;
private fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index;
public static MediaDetailFragment forMedia(int index) {
return forMedia(index, false);
}
public static MediaDetailFragment forMedia(int index, boolean editable) {
MediaDetailFragment mf = new MediaDetailFragment();
Bundle state = new Bundle();
state.putBoolean("editable", editable);
state.putInt("index", index);
state.putInt("listIndex", 0);
state.putInt("listTop", 0);
mf.setArguments(state);
return mf;
}
private ImageView image;
//private EditText title;
private ProgressBar loadingProgress;
private ImageView loadingFailed;
private fr.free.nrw.commons.media.MediaDetailSpacer spacer;
private int initialListTop = 0;
private TextView title;
private TextView desc;
private TextView license;
private LinearLayout categoryContainer;
private ScrollView scrollView;
private ArrayList<String> categoryNames;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener;
DataSetObserver dataObserver;
private AsyncTask<Void,Void,Boolean> detailFetchTask;
private LicenseList licenseList;
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("index", index);
outState.putBoolean("editable", editable);
getScrollPosition();
outState.putInt("listTop", initialListTop);
}
private void getScrollPosition() {
initialListTop = scrollView.getScrollY();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
detailProvider = (fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider)getActivity();
if(savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
index = savedInstanceState.getInt("index");
initialListTop = savedInstanceState.getInt("listTop");
} else {
editable = getArguments().getBoolean("editable");
index = getArguments().getInt("index");
initialListTop = 0;
}
categoryNames = new ArrayList<String>();
categoryNames.add(getString(R.string.detail_panel_cats_loading));
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
image = (ImageView) view.findViewById(R.id.mediaDetailImage);
loadingProgress = (ProgressBar) view.findViewById(R.id.mediaDetailImageLoading);
loadingFailed = (ImageView) view.findViewById(R.id.mediaDetailImageFailed);
scrollView = (ScrollView) view.findViewById(R.id.mediaDetailScrollView);
// Detail consists of a list view with main pane in header view, plus category list.
spacer = (fr.free.nrw.commons.media.MediaDetailSpacer) view.findViewById(R.id.mediaDetailSpacer);
title = (TextView) view.findViewById(R.id.mediaDetailTitle);
desc = (TextView) view.findViewById(R.id.mediaDetailDesc);
license = (TextView) view.findViewById(R.id.mediaDetailLicense);
categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer);
licenseList = new LicenseList(getActivity());
Media media = detailProvider.getMediaAtPosition(index);
if (media == null) {
// Ask the detail provider to ping us when we're ready
Log.d("Commons", "MediaDetailFragment not yet ready to display details; registering observer");
dataObserver = new DataSetObserver() {
public void onChanged() {
Log.d("Commons", "MediaDetailFragment ready to display delayed details!");
detailProvider.unregisterDataSetObserver(dataObserver);
dataObserver = null;
displayMediaDetails(detailProvider.getMediaAtPosition(index));
}
};
detailProvider.registerDataSetObserver(dataObserver);
} else {
Log.d("Commons", "MediaDetailFragment ready to display details");
displayMediaDetails(media);
}
// Progressively darken the image in the background when we scroll detail pane up
scrollListener = new ViewTreeObserver.OnScrollChangedListener() {
public void onScrollChanged() {
updateTheDarkness();
}
};
view.getViewTreeObserver().addOnScrollChangedListener(scrollListener);
// Layout layoutListener to size the spacer item relative to the available space.
// There may be a .... better way to do this.
layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
private int currentHeight = -1;
public void onGlobalLayout() {
int viewHeight = view.getHeight();
//int textHeight = title.getLineHeight();
int paddingDp = 112;
float paddingPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingDp, getResources().getDisplayMetrics());
int newHeight = viewHeight - Math.round(paddingPx);
if (newHeight != currentHeight) {
currentHeight = newHeight;
ViewGroup.LayoutParams params = spacer.getLayoutParams();
params.height = newHeight;
spacer.setLayoutParams(params);
scrollView.scrollTo(0, initialListTop);
}
}
};
view.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);
return view;
}
private void displayMediaDetails(final Media media) {
String actualUrl = (media.getLocalUri() != null && !TextUtils.isEmpty(media.getLocalUri().toString())) ? media.getLocalUri().toString() : media.getThumbnailUrl(640);
if(actualUrl.startsWith("http")) {
ImageLoader loader = ((CommonsApplication)getActivity().getApplicationContext()).getImageLoader();
MediaWikiImageView mwImage = (MediaWikiImageView)image;
mwImage.setLoadingView(loadingProgress); //FIXME: Set this as an attribute
mwImage.setMedia(media, loader);
Log.d("Volley", actualUrl);
// FIXME: For transparent images
// Load image metadata: desc, license, categories
// FIXME: keep the spinner going while we load data
// FIXME: cache this data
detailFetchTask = new AsyncTask<Void, Void, Boolean>() {
private MediaDataExtractor extractor;
@Override
protected void onPreExecute() {
extractor = new MediaDataExtractor(media.getFilename(), licenseList);
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
extractor.fetch();
return Boolean.TRUE;
} catch (IOException e) {
e.printStackTrace();
}
return Boolean.FALSE;
}
@Override
protected void onPostExecute(Boolean success) {
detailFetchTask = null;
if (success.booleanValue()) {
extractor.fill(media);
// Fill some fields
desc.setText(prettyDescription(media));
license.setText(prettyLicense(media));
categoryNames.removeAll(categoryNames);
categoryNames.addAll(media.getCategories());
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList();
} else {
Log.d("Commons", "Failed to load photo details.");
}
}
};
Utils.executeAsyncTask(detailFetchTask);
} else {
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl, image, displayOptions, new ImageLoadingListener() {
public void onLoadingStarted(String s, View view) {
loadingProgress.setVisibility(View.VISIBLE);
}
public void onLoadingFailed(String s, View view, FailReason failReason) {
loadingProgress.setVisibility(View.GONE);
loadingFailed.setVisibility(View.VISIBLE);
}
public void onLoadingComplete(String s, View view, Bitmap bitmap) {
loadingProgress.setVisibility(View.GONE);
loadingFailed.setVisibility(View.GONE);
image.setVisibility(View.VISIBLE);
if(bitmap.hasAlpha()) {
image.setBackgroundResource(android.R.color.white);
}
}
public void onLoadingCancelled(String s, View view) {
throw new RuntimeException("Image loading cancelled. But why?");
}
});
}
title.setText(media.getDisplayTitle());
desc.setText(""); // fill in from network...
license.setText(""); // fill in from network...
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
displayOptions = Utils.getGenericDisplayOptions().build();
}
@Override
public void onDestroyView() {
if (detailFetchTask != null) {
detailFetchTask.cancel(true);
detailFetchTask = null;
}
if (layoutListener != null) {
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
layoutListener = null;
}
if (scrollListener != null) {
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
scrollListener = null;
}
if (dataObserver != null) {
detailProvider.unregisterDataSetObserver(dataObserver);
dataObserver = null;
}
super.onDestroyView();
}
private void rebuildCatList() {
// @fixme add the category items
for (String cat : categoryNames) {
View catLabel = buildCatLabel(cat);
categoryContainer.addView(catLabel);
}
}
private View buildCatLabel(String cat) {
final String catName = cat;
final View item = getLayoutInflater(null).inflate(R.layout.detail_category_item, null, false);
final TextView textView = (TextView)item.findViewById(R.id.mediaDetailCategoryItemText);
textView.setText(cat);
if (categoriesLoaded && categoriesPresent) {
textView.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
String selectedCategoryTitle = "Category:" + catName;
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(Utils.uriForWikiPage(selectedCategoryTitle));
startActivity(viewIntent);
}
});
}
return item;
}
private void updateTheDarkness() {
// You must face the darkness alone
int scrollY = scrollView.getScrollY();
int scrollMax = getView().getHeight();
float scrollPercentage = (float)scrollY / (float)scrollMax;
final float transparencyMax = 0.75f;
if (scrollPercentage > transparencyMax) {
scrollPercentage = transparencyMax;
}
image.setAlpha(1.0f - scrollPercentage);
}
private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available
String desc = media.getDescription("en").trim();
if (desc.equals("")) {
return getString(R.string.detail_description_empty);
} else {
return desc;
}
}
private String prettyLicense(Media media) {
String licenseKey = media.getLicense();
Log.d("Commons", "Media license is: " + licenseKey);
if (licenseKey == null || licenseKey.equals("")) {
return getString(R.string.detail_license_empty);
}
License licenseObj = licenseList.get(licenseKey);
if (licenseObj == null) {
return licenseKey;
} else {
return licenseObj.getName();
}
}
}

View file

@ -0,0 +1,287 @@
package fr.free.nrw.commons.media;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
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 fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener {
private ViewPager pager;
private Boolean editable;
private CommonsApplication app;
public void onPageScrolled(int i, float v, int i2) {
getActivity().supportInvalidateOptionsMenu();
}
public void onPageSelected(int i) {
}
public void onPageScrollStateChanged(int i) {
}
public interface MediaDetailProvider {
public Media getMediaAtPosition(int i);
public int getTotalMediaCount();
public void notifyDatasetChanged();
public void registerDataSetObserver(DataSetObserver observer);
public void unregisterDataSetObserver(DataSetObserver observer);
}
private class MediaDetailAdapter extends FragmentStatePagerAdapter {
public MediaDetailAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int i) {
if(i == 0) {
// See bug https://code.google.com/p/android/issues/detail?id=27526
pager.postDelayed(new Runnable() {
public void run() {
getActivity().supportInvalidateOptionsMenu();
}
}, 5);
}
return fr.free.nrw.commons.media.MediaDetailFragment.forMedia(i, editable);
}
@Override
public int getCount() {
return ((MediaDetailProvider)getActivity()).getTotalMediaCount();
}
}
public MediaDetailPagerFragment() {
this(false);
}
@SuppressLint("ValidFragment")
public MediaDetailPagerFragment(Boolean editable) {
this.editable = editable;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_media_detail_pager, container, false);
pager = (ViewPager) view.findViewById(R.id.mediaDetailsPager);
pager.setOnPageChangeListener(this);
if(savedInstanceState != null) {
final int pageNumber = savedInstanceState.getInt("current-page");
// Adapter doesn't seem to be loading immediately.
// Dear God, please forgive us for our sins
view.postDelayed(new Runnable() {
public void run() {
pager.setAdapter(new MediaDetailAdapter(getChildFragmentManager()));
pager.setCurrentItem(pageNumber, false);
getActivity().supportInvalidateOptionsMenu();
}
}, 100);
} else {
pager.setAdapter(new MediaDetailAdapter(getChildFragmentManager()));
}
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("current-page", pager.getCurrentItem());
outState.putBoolean("editable", editable);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
}
app = (CommonsApplication)getActivity().getApplicationContext();
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
MediaDetailProvider provider = (MediaDetailProvider)getActivity();
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
switch(item.getItemId()) {
case R.id.menu_share_current_image:
EventLog.schema(CommonsApplication.EVENT_SHARE_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("filename", m.getFilename())
.log();
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " " + m.getDescriptionUrl());
startActivity(shareIntent);
return true;
case R.id.menu_browser_current_image:
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(Uri.parse(m.getDescriptionUrl()));
startActivity(viewIntent);
return true;
case R.id.menu_download_current_image:
downloadMedia(m);
return true;
case R.id.menu_retry_current_image:
// Is this... sane? :)
((ContributionsActivity)getActivity()).retryUpload(pager.getCurrentItem());
getActivity().getSupportFragmentManager().popBackStack();
return true;
case R.id.menu_cancel_current_image:
// todo: delete image
((ContributionsActivity)getActivity()).deleteUpload(pager.getCurrentItem());
getActivity().getSupportFragmentManager().popBackStack();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Start the media file downloading to the local SD card/storage.
* The file can then be opened in Gallery or other apps.
*
* @param m
*/
private void downloadMedia(Media m) {
String imageUrl = m.getImageUrl(),
fileName = m.getFilename();
// Strip 'File:' from beginning of filename, we really shouldn't store it
fileName = fileName.replaceFirst("^File:", "");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// Gingerbread DownloadManager has no HTTPS support...
// Download file over HTTP, there'll be no credentials
// sent so it should be safe-ish.
imageUrl = imageUrl.replaceFirst("^https://", "http://");
}
Uri imageUri = Uri.parse(imageUrl);
DownloadManager.Request req = new DownloadManager.Request(imageUri);
req.setDescription(getString(R.string.app_name));
req.setTitle(m.getDisplayTitle());
req.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// Modern Android updates the gallery automatically. Yay!
req.allowScanningByMediaScanner();
// On HC/ICS/JB we can leave the download notification up when complete.
// This allows folks to open the file directly in gallery viewer.
// But for some reason it fails on Honeycomb (Google TV). Sigh.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
}
}
final DownloadManager manager = (DownloadManager)getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
final long downloadId = manager.enqueue(req);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
// For Gingerbread compatibility...
BroadcastReceiver onComplete = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// Check if the download has completed...
Cursor c = manager.query(new DownloadManager.Query()
.setFilterById(downloadId)
.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL | DownloadManager.STATUS_FAILED)
);
if (c.moveToFirst()) {
int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
Log.d("Commons", "Download completed with status " + status);
if (status == DownloadManager.STATUS_SUCCESSFUL) {
// Force Gallery to index the new file
Uri mediaUri = Uri.parse("file://" + Environment.getExternalStorageDirectory());
getActivity().sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, mediaUri));
// todo: show a persistent notification?
}
} else {
Log.d("Commons", "Couldn't get download status for some reason");
}
getActivity().unregisterReceiver(this);
}
};
getActivity().registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if(!editable) { // Disable menu options for editable views
menu.clear(); // see http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_image_detail, menu);
if(pager != null) {
MediaDetailProvider provider = (MediaDetailProvider)getActivity();
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
if(m != null) {
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
menu.findItem(R.id.menu_retry_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_cancel_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
if(m instanceof Contribution) {
Contribution c = (Contribution)m;
switch(c.getState()) {
case Contribution.STATE_FAILED:
menu.findItem(R.id.menu_retry_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_cancel_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_browser_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_share_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_download_current_image).setEnabled(false).setVisible(false);
break;
case Contribution.STATE_IN_PROGRESS:
case Contribution.STATE_QUEUED:
menu.findItem(R.id.menu_retry_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_cancel_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_browser_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_share_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_download_current_image).setEnabled(false).setVisible(false);
break;
case Contribution.STATE_COMPLETED:
// Default set of menu items works fine. Treat same as regular media object
break;
}
}
return;
}
}
}
}
public void showImage(int i) {
pager.setCurrentItem(i);
}
}

View file

@ -0,0 +1,19 @@
package fr.free.nrw.commons.media;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
public class MediaDetailSpacer extends View {
public MediaDetailSpacer(Context context) {
super(context);
}
public MediaDetailSpacer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MediaDetailSpacer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}