/*
* Materialize - Materialize all those not material
* Copyright (C) 2015 XiNGRZ <xxx@oxo.ooo>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ooo.oxo.apps.materialize;
import android.Manifest;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.databinding.DataBindingUtil;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.v7.widget.PopupMenu;
import android.view.Menu;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Toast;
import com.jakewharton.rxbinding.view.RxMenuItem;
import com.jakewharton.rxbinding.view.RxView;
import com.tbruyelle.rxpermissions.RxPermissions;
import com.trello.rxlifecycle.components.support.RxAppCompatActivity;
import com.umeng.analytics.MobclickAgent;
import java.util.HashMap;
import io.realm.Realm;
import ooo.oxo.apps.materialize.databinding.AdjustActivityBinding;
import ooo.oxo.apps.materialize.db.Adjustment;
import ooo.oxo.apps.materialize.graphics.CompositeDrawable;
import ooo.oxo.apps.materialize.graphics.InfiniteDrawable;
import ooo.oxo.apps.materialize.graphics.ShapeDrawable;
import ooo.oxo.apps.materialize.graphics.TransparencyDrawable;
import ooo.oxo.apps.materialize.io.IconCacheManager;
import ooo.oxo.apps.materialize.io.PublicIconManager;
import ooo.oxo.apps.materialize.util.LauncherUtil;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class AdjustActivity extends RxAppCompatActivity {
private static final boolean SUPPORT_MIPMAP = Build.VERSION.SDK_INT >= 18;
/**
* Icon size in pixel since Android 4.3 (18) with mipmaps support
* It's the actual size of drawables in drawable-anydpi-v18 folder
*/
private static final int LAUNCHER_SIZE_MIPMAP = 192;
private int index;
private AdjustActivityBinding binding;
private IconCacheManager iconCacheManager;
private PublicIconManager publicIconManager;
private AdjustViewModel viewModel = new AdjustViewModel();
private Realm realm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
index = getIntent().getIntExtra("index", 0);
binding = DataBindingUtil.setContentView(this, R.layout.adjust_activity);
binding.setAdjust(viewModel);
binding.setTransparency(new TransparencyDrawable(
getResources(), R.dimen.transparency_grid_size));
RadioGroup shapes = binding.shape;
for (int i = 0; i < shapes.getChildCount(); i++) {
RadioButton child = (RadioButton) shapes.getChildAt(i);
CompositeDrawable.Shape shape = viewModel.mapShape(child.getId());
child.setButtonDrawable(new ShapeDrawable(getResources(), shape, R.color.accent));
child.setBackgroundDrawable(null);
}
PopupMenu popup = new PopupMenu(this, binding.more);
popup.inflate(R.menu.adjust);
Menu menu = popup.getMenu();
binding.more.setOnClickListener(v -> popup.show());
iconCacheManager = new IconCacheManager(this);
publicIconManager = new PublicIconManager(this);
ActivityInfo activity = getIntent().getParcelableExtra("activity");
Observable<AppInfo> resolving = Observable.just(activity)
.compose(bindToLifecycle())
.observeOn(Schedulers.io())
.map(act -> AppInfo.from(act, getPackageManager()))
.filter(app -> app != null)
.filter(AppInfo::resolveIcon)
.cache();
resolving
.observeOn(AndroidSchedulers.mainThread())
.subscribe(binding::setApp);
Observable<InfiniteDrawable> infinity = resolving
.compose(bindToLifecycle())
.observeOn(Schedulers.computation())
.map(app -> InfiniteDrawable.from(app.icon))
.filter(drawable -> drawable != null)
.cache();
infinity
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::setInfinite);
realm = Realm.getInstance(this);
Observable<Adjustment> adjustment = resolving
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.map(app -> realm.where(Adjustment.class)
.equalTo("component", app.component.flattenToString())
.findFirst())
.cache();
adjustment.map(model -> model == null)
.doOnNext(binding::setIsNew)
.map(isNew -> !isNew)
.subscribe(RxMenuItem.visible(menu.findItem(R.id.re_add_to_home)));
adjustment.filter(model -> model != null)
.zipWith(infinity, (model, drawable) -> model)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::applyFromModel);
RxView.clicks(binding.cancel)
.compose(bindToLifecycle())
.subscribe(avoid -> {
finishWithResult(RESULT_CANCELED);
});
int size = SUPPORT_MIPMAP ? LAUNCHER_SIZE_MIPMAP
: getResources().getDimensionPixelSize(R.dimen.launcher_size);
Observable<Bitmap> renders = Observable.just(binding.result.getComposite())
.compose(bindToLifecycle())
.observeOn(Schedulers.computation())
.map(compose -> {
Bitmap icon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
compose.drawTo(new Canvas(icon), SUPPORT_MIPMAP);
return icon;
})
.observeOn(Schedulers.io())
.zipWith(resolving, (icon, app) -> {
iconCacheManager.save(app, icon);
return icon;
});
Observable<Adjustment> persist = adjustment
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.zipWith(resolving, (model, app) -> {
realm.beginTransaction();
if (model == null || !model.isValid()) {
model = realm.createObject(Adjustment.class);
model.setComponent(app.component.flattenToString());
}
model.setShape(viewModel.getShapeModelValue());
model.setPadding(viewModel.getPadding());
model.setColor(viewModel.getBackgroundModelValue());
realm.commitTransaction();
return model;
});
RxView.clicks(binding.save)
.compose(bindToLifecycle())
.zipWith(resolving, (avoid, app) -> app)
.flatMap(avoid -> Observable.zip(renders, persist, (icon, ad) -> icon))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(avoid -> {
Toast.makeText(this, R.string.toast_saved, Toast.LENGTH_SHORT).show();
MobclickAgent.onEvent(this, "compose", makeEvent("none"));
finishWithResult(RESULT_OK);
});
Observable.merge(RxView.clicks(binding.install), RxMenuItem.clicks(menu.findItem(R.id.re_add_to_home)))
.compose(bindToLifecycle())
.zipWith(resolving, (avoid, app) -> app)
.flatMap(avoid -> Observable.zip(renders, persist, (icon, ad) -> icon))
.observeOn(AndroidSchedulers.mainThread())
.zipWith(resolving, (icon, app) -> {
LauncherUtil.installShortcut(this, app.getIntent(), app.label, icon);
return null;
})
.subscribe(avoid -> {
Toast.makeText(this, R.string.toast_added_to_home, Toast.LENGTH_SHORT).show();
MobclickAgent.onEvent(this, "compose", makeEvent("launcher"));
MobclickAgent.onEvent(this, "install");
finishWithResult(RESULT_OK);
});
Observable<Boolean> permission = RxPermissions
.getInstance(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.doOnNext(RxMenuItem.enabled(menu.findItem(R.id.export_to_gallery)))
.filter(granted -> granted);
RxMenuItem.clicks(menu.findItem(R.id.export_to_gallery))
.compose(bindToLifecycle())
.zipWith(resolving, (avoid, app) -> app)
.flatMap(avoid -> Observable.zip(renders, persist, (icon, ad) -> icon))
.observeOn(AndroidSchedulers.mainThread())
.zipWith(permission, (icon, granted) -> icon)
.observeOn(Schedulers.io())
.zipWith(resolving, (icon, app) -> {
publicIconManager.save(app, icon);
return null;
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(avoid -> {
Toast.makeText(this, R.string.toast_exported_to_gallery, Toast.LENGTH_SHORT).show();
MobclickAgent.onEvent(this, "compose", makeEvent("gallery"));
finishWithResult(RESULT_OK);
});
Observable<AppInfo> deleteCache = resolving
.compose(bindToLifecycle())
.observeOn(Schedulers.io())
.doOnNext(iconCacheManager::delete);
Observable<Adjustment> deletePersist = adjustment
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(model -> {
if (model != null) {
realm.beginTransaction();
model.removeFromRealm();
realm.commitTransaction();
}
});
RxMenuItem.clicks(menu.findItem(R.id.reset))
.compose(bindToLifecycle())
.zipWith(resolving, (avoid, app) -> app)
.flatMap(avoid -> Observable.zip(deleteCache, deletePersist, (a, b) -> null))
.doOnNext(avoid -> viewModel.reset())
.subscribe(avoid -> {
MobclickAgent.onEvent(this, "reset");
finishWithResult(RESULT_CANCELED);
});
}
private void finishWithResult(int resultCode) {
setResult(resultCode, new Intent().putExtra("index", index));
supportFinishAfterTransition();
}
@Override
protected void onResume() {
super.onResume();
MobclickAgent.onResume(this);
}
@Override
protected void onPause() {
super.onPause();
MobclickAgent.onPause(this);
}
private HashMap<String, String> makeEvent(String usage) {
HashMap<String, String> event = new HashMap<>();
event.put("shape", viewModel.getShape().name());
event.put("color", mapColorName(binding.colors.getCheckedRadioButtonId()));
event.put("usage", usage);
return event;
}
private String mapColorName(@IdRes int radio) {
switch (radio) {
case R.id.color_white:
return "white";
case R.id.color_infinite:
return "infinite";
default:
return null;
}
}
}