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