1 /*
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 
17 package com.android.egg.neko;
18 
19 import android.Manifest;
20 import android.app.ActionBar;
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.content.DialogInterface.OnClickListener;
26 import android.content.Intent;
27 import android.content.pm.PackageManager;
28 import android.graphics.Bitmap;
29 import android.graphics.Color;
30 import android.graphics.drawable.Drawable;
31 import android.media.MediaScannerConnection;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.Environment;
35 import android.util.Log;
36 import android.view.ContextThemeWrapper;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.View.OnLongClickListener;
40 import android.view.ViewGroup;
41 import android.widget.EditText;
42 import android.widget.ImageView;
43 import android.widget.TextView;
44 
45 import androidx.core.content.FileProvider;
46 import androidx.recyclerview.widget.GridLayoutManager;
47 import androidx.recyclerview.widget.RecyclerView;
48 
49 import com.android.egg.R;
50 import com.android.egg.neko.PrefState.PrefsListener;
51 import com.android.internal.logging.MetricsLogger;
52 
53 import java.io.File;
54 import java.io.FileOutputStream;
55 import java.io.IOException;
56 import java.io.OutputStream;
57 import java.util.Collections;
58 import java.util.Comparator;
59 import java.util.List;
60 
61 public 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
onCreate(Bundle savedInstanceState)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
onDestroy()99     protected void onDestroy() {
100         super.onDestroy();
101         mPrefs.setListener(null);
102     }
103 
updateCats()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 
onCatClick(Cat cat)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 
onCatRemove(Cat cat)142     private void onCatRemove(Cat cat) {
143         cat.logRemove(this);
144         mPrefs.removeCat(cat);
145     }
146 
showNameDialog(final Cat cat)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
onPrefsChanged()173     public void onPrefsChanged() {
174         updateCats();
175     }
176 
177     private class CatAdapter extends RecyclerView.Adapter<CatHolder> {
178 
179         private Cat[] mCats;
180 
setCats(Cat[] cats)181         public void setCats(Cat[] cats) {
182             mCats = cats;
183             notifyDataSetChanged();
184         }
185 
186         @Override
onCreateViewHolder(ViewGroup parent, int viewType)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 
setContextGroupVisible(final CatHolder holder, boolean vis)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
onBindViewHolder(final CatHolder holder, int position)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
getItemCount()271         public int getItemCount() {
272             return mCats.length;
273         }
274     }
275 
shareCat(Cat cat)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
onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)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 
CatHolder(View itemView)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 }
341