blob: 8ed808760dcda0ec73d927e4c44770408c83abfa [file] [log] [blame]
Dan Sandlerf4e83e02020-05-12 21:25:31 -04001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.egg.neko;
18
19import android.Manifest;
20import android.app.ActionBar;
21import android.app.Activity;
22import android.app.AlertDialog;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.content.DialogInterface.OnClickListener;
26import android.content.Intent;
27import android.content.pm.PackageManager;
28import android.graphics.Bitmap;
29import android.graphics.Color;
30import android.graphics.drawable.Drawable;
31import android.media.MediaScannerConnection;
32import android.net.Uri;
33import android.os.Bundle;
34import android.os.Environment;
35import android.util.Log;
36import android.view.ContextThemeWrapper;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.View.OnLongClickListener;
40import android.view.ViewGroup;
41import android.widget.EditText;
42import android.widget.ImageView;
43import android.widget.TextView;
44
45import androidx.core.content.FileProvider;
46import androidx.recyclerview.widget.GridLayoutManager;
47import androidx.recyclerview.widget.RecyclerView;
48
49import com.android.egg.R;
50import com.android.egg.neko.PrefState.PrefsListener;
51import com.android.internal.logging.MetricsLogger;
52
53import java.io.File;
54import java.io.FileOutputStream;
55import java.io.IOException;
56import java.io.OutputStream;
57import java.util.Collections;
58import java.util.Comparator;
59import java.util.List;
60
61public class NekoLand extends Activity implements PrefsListener {
62 public static String CHAN_ID = "EGG";
63
64 public static boolean DEBUG = false;
65 public static boolean DEBUG_NOTIFICATIONS = false;
66
67 private static final int EXPORT_BITMAP_SIZE = 600;
68
69 private static final int STORAGE_PERM_REQUEST = 123;
70
71 private static boolean CAT_GEN = false;
72 private PrefState mPrefs;
73 private CatAdapter mAdapter;
74 private Cat mPendingShareCat;
75
76
77 @Override
78 public void onCreate(Bundle savedInstanceState) {
79 super.onCreate(savedInstanceState);
80 setContentView(R.layout.neko_activity);
81 final ActionBar actionBar = getActionBar();
82 if (actionBar != null) {
83 actionBar.setLogo(Cat.create(this));
84 actionBar.setDisplayUseLogoEnabled(false);
85 actionBar.setDisplayShowHomeEnabled(true);
86 }
87
88 mPrefs = new PrefState(this);
89 mPrefs.setListener(this);
90 final RecyclerView recyclerView = findViewById(R.id.holder);
91 mAdapter = new CatAdapter();
92 recyclerView.setAdapter(mAdapter);
93 recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
94 int numCats = updateCats();
95 MetricsLogger.histogram(this, "egg_neko_visit_gallery", numCats);
96 }
97
98 @Override
99 protected void onDestroy() {
100 super.onDestroy();
101 mPrefs.setListener(null);
102 }
103
104 private int updateCats() {
105 Cat[] cats;
106 if (CAT_GEN) {
107 cats = new Cat[50];
108 for (int i = 0; i < cats.length; i++) {
109 cats[i] = Cat.create(this);
110 }
111 } else {
112 final float[] hsv = new float[3];
113 List<Cat> list = mPrefs.getCats();
114 Collections.sort(list, new Comparator<Cat>() {
115 @Override
116 public int compare(Cat cat, Cat cat2) {
117 Color.colorToHSV(cat.getBodyColor(), hsv);
118 float bodyH1 = hsv[0];
119 Color.colorToHSV(cat2.getBodyColor(), hsv);
120 float bodyH2 = hsv[0];
121 return Float.compare(bodyH1, bodyH2);
122 }
123 });
124 cats = list.toArray(new Cat[0]);
125 }
126 mAdapter.setCats(cats);
127 return cats.length;
128 }
129
130 private void onCatClick(Cat cat) {
131 if (CAT_GEN) {
132 mPrefs.addCat(cat);
133 new AlertDialog.Builder(NekoLand.this)
134 .setTitle("Cat added")
135 .setPositiveButton(android.R.string.ok, null)
136 .show();
137 } else {
138 showNameDialog(cat);
139 }
140 }
141
142 private void onCatRemove(Cat cat) {
143 cat.logRemove(this);
144 mPrefs.removeCat(cat);
145 }
146
147 private void showNameDialog(final Cat cat) {
148 final Context context = new ContextThemeWrapper(this,
149 android.R.style.Theme_Material_Light_Dialog_NoActionBar);
150 // TODO: Move to XML, add correct margins.
151 View view = LayoutInflater.from(context).inflate(R.layout.edit_text, null);
152 final EditText text = (EditText) view.findViewById(android.R.id.edit);
153 text.setText(cat.getName());
154 text.setSelection(cat.getName().length());
155 final int size = context.getResources()
156 .getDimensionPixelSize(android.R.dimen.app_icon_size);
157 Drawable catIcon = cat.createIcon(this, size, size).loadDrawable(this);
158 new AlertDialog.Builder(context)
159 .setTitle(" ")
160 .setIcon(catIcon)
161 .setView(view)
162 .setPositiveButton(android.R.string.ok, new OnClickListener() {
163 @Override
164 public void onClick(DialogInterface dialog, int which) {
165 cat.logRename(context);
166 cat.setName(text.getText().toString().trim());
167 mPrefs.addCat(cat);
168 }
169 }).show();
170 }
171
172 @Override
173 public void onPrefsChanged() {
174 updateCats();
175 }
176
177 private class CatAdapter extends RecyclerView.Adapter<CatHolder> {
178
179 private Cat[] mCats;
180
181 public void setCats(Cat[] cats) {
182 mCats = cats;
183 notifyDataSetChanged();
184 }
185
186 @Override
187 public CatHolder onCreateViewHolder(ViewGroup parent, int viewType) {
188 return new CatHolder(LayoutInflater.from(parent.getContext())
189 .inflate(R.layout.cat_view, parent, false));
190 }
191
192 private void setContextGroupVisible(final CatHolder holder, boolean vis) {
193 final View group = holder.contextGroup;
194 if (vis && group.getVisibility() != View.VISIBLE) {
195 group.setAlpha(0);
196 group.setVisibility(View.VISIBLE);
197 group.animate().alpha(1.0f).setDuration(333);
198 Runnable hideAction = new Runnable() {
199 @Override
200 public void run() {
201 setContextGroupVisible(holder, false);
202 }
203 };
204 group.setTag(hideAction);
205 group.postDelayed(hideAction, 5000);
206 } else if (!vis && group.getVisibility() == View.VISIBLE) {
207 group.removeCallbacks((Runnable) group.getTag());
208 group.animate().alpha(0f).setDuration(250).withEndAction(new Runnable() {
209 @Override
210 public void run() {
211 group.setVisibility(View.INVISIBLE);
212 }
213 });
214 }
215 }
216
217 @Override
218 public void onBindViewHolder(final CatHolder holder, int position) {
219 Context context = holder.itemView.getContext();
220 final int size = context.getResources().getDimensionPixelSize(R.dimen.neko_display_size);
221 holder.imageView.setImageIcon(mCats[position].createIcon(context, size, size));
222 holder.textView.setText(mCats[position].getName());
223 holder.itemView.setOnClickListener(new View.OnClickListener() {
224 @Override
225 public void onClick(View v) {
226 onCatClick(mCats[holder.getAdapterPosition()]);
227 }
228 });
229 holder.itemView.setOnLongClickListener(new OnLongClickListener() {
230 @Override
231 public boolean onLongClick(View v) {
232 setContextGroupVisible(holder, true);
233 return true;
234 }
235 });
236 holder.delete.setOnClickListener(new View.OnClickListener() {
237 @Override
238 public void onClick(View v) {
239 setContextGroupVisible(holder, false);
240 new AlertDialog.Builder(NekoLand.this)
241 .setTitle(getString(R.string.confirm_delete, mCats[position].getName()))
242 .setNegativeButton(android.R.string.cancel, null)
243 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
244 @Override
245 public void onClick(DialogInterface dialog, int which) {
246 onCatRemove(mCats[holder.getAdapterPosition()]);
247 }
248 })
249 .show();
250 }
251 });
252 holder.share.setOnClickListener(new View.OnClickListener() {
253 @Override
254 public void onClick(View v) {
255 setContextGroupVisible(holder, false);
256 Cat cat = mCats[holder.getAdapterPosition()];
257 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
258 != PackageManager.PERMISSION_GRANTED) {
259 mPendingShareCat = cat;
260 requestPermissions(
261 new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
262 STORAGE_PERM_REQUEST);
263 return;
264 }
265 shareCat(cat);
266 }
267 });
268 }
269
270 @Override
271 public int getItemCount() {
272 return mCats.length;
273 }
274 }
275
276 private void shareCat(Cat cat) {
277 final File dir = new File(
278 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
279 "Cats");
280 if (!dir.exists() && !dir.mkdirs()) {
281 Log.e("NekoLand", "save: error: can't create Pictures directory");
282 return;
283 }
284 final File png = new File(dir, cat.getName().replaceAll("[/ #:]+", "_") + ".png");
285 Bitmap bitmap = cat.createBitmap(EXPORT_BITMAP_SIZE, EXPORT_BITMAP_SIZE);
286 if (bitmap != null) {
287 try {
288 OutputStream os = new FileOutputStream(png);
289 bitmap.compress(Bitmap.CompressFormat.PNG, 0, os);
290 os.close();
291 MediaScannerConnection.scanFile(
292 this,
293 new String[]{png.toString()},
294 new String[]{"image/png"},
295 null);
296 Log.v("Neko", "cat file: " + png);
297 Uri uri = FileProvider.getUriForFile(this, "com.android.egg.fileprovider", png);
298 Log.v("Neko", "cat uri: " + uri);
299 Intent intent = new Intent(Intent.ACTION_SEND);
300 intent.putExtra(Intent.EXTRA_STREAM, uri);
301 intent.putExtra(Intent.EXTRA_SUBJECT, cat.getName());
302 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
303 intent.setType("image/png");
304 startActivity(Intent.createChooser(intent, null)
305 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION));
306 cat.logShare(this);
307 } catch (IOException e) {
308 Log.e("NekoLand", "save: error: " + e);
309 }
310 }
311 }
312
313 @Override
314 public void onRequestPermissionsResult(int requestCode,
315 String permissions[], int[] grantResults) {
316 if (requestCode == STORAGE_PERM_REQUEST) {
317 if (mPendingShareCat != null) {
318 shareCat(mPendingShareCat);
319 mPendingShareCat = null;
320 }
321 }
322 }
323
324 private static class CatHolder extends RecyclerView.ViewHolder {
325 private final ImageView imageView;
326 private final TextView textView;
327 private final View contextGroup;
328 private final View delete;
329 private final View share;
330
331 public CatHolder(View itemView) {
332 super(itemView);
333 imageView = (ImageView) itemView.findViewById(android.R.id.icon);
334 textView = (TextView) itemView.findViewById(android.R.id.title);
335 contextGroup = itemView.findViewById(R.id.contextGroup);
336 delete = itemView.findViewById(android.R.id.closeButton);
337 share = itemView.findViewById(android.R.id.shareText);
338 }
339 }
340}