1 /*
2  * Copyright 2019 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.quares
18 
19 import android.app.Activity
20 import android.content.Context
21 import android.content.res.Configuration
22 import android.graphics.Canvas
23 import android.graphics.Paint
24 import android.graphics.Typeface
25 import android.graphics.drawable.Icon
26 import android.os.Bundle
27 import android.text.StaticLayout
28 import android.text.TextPaint
29 import android.util.Log
30 import android.view.View
31 import android.view.View.GONE
32 import android.view.View.VISIBLE
33 import android.widget.Button
34 import android.widget.CompoundButton
35 import android.widget.GridLayout
36 
37 import java.util.Random
38 
39 import com.android.egg.R
40 
41 const val TAG = "Quares"
42 
43 class QuaresActivity : Activity() {
44     private var q: Quare = Quare(16, 16, 1)
45     private var resId = 0
46     private var resName = ""
47     private var icon: Icon? = null
48 
49     private lateinit var label: Button
50     private lateinit var grid: GridLayout
51 
onCreatenull52     override fun onCreate(savedInstanceState: Bundle?) {
53         super.onCreate(savedInstanceState)
54 
55         window.decorView.systemUiVisibility =
56                 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
57 
58         actionBar?.hide()
59 
60         setContentView(R.layout.activity_quares)
61 
62         grid = findViewById(R.id.grid)
63         label = findViewById(R.id.label)
64 
65         if (savedInstanceState != null) {
66             Log.v(TAG, "restoring puzzle from state")
67             q = savedInstanceState.getParcelable("q") ?: q
68             resId = savedInstanceState.getInt("resId")
69             resName = savedInstanceState.getString("resName", "")
70             loadPuzzle()
71         }
72 
73         label.setOnClickListener { newPuzzle() }
74     }
75 
onResumenull76     override fun onResume() {
77         super.onResume()
78         if (resId == 0) {
79             // lazy init from onCreate
80             newPuzzle()
81         }
82         checkVictory()
83     }
84 
onSaveInstanceStatenull85     override fun onSaveInstanceState(outState: Bundle) {
86         super.onSaveInstanceState(outState)
87 
88         outState.putParcelable("q", q)
89         outState.putInt("resId", resId)
90         outState.putString("resName", resName)
91     }
92 
newPuzzlenull93     fun newPuzzle() {
94         Log.v(TAG, "new puzzle...")
95 
96         q.resetUserMarks()
97         val oldResId = resId
98         resId = android.R.drawable.stat_sys_warning
99         try {
100             for (tries in 0..3) {
101                 val ar = resources.obtainTypedArray(R.array.puzzles)
102                 val newName = ar.getString(Random().nextInt(ar.length()))
103                 if (newName == null) continue
104 
105                 Log.v(TAG, "Looking for icon " + newName)
106 
107                 val pkg = getPackageNameForResourceName(newName)
108                 val newId = packageManager.getResourcesForApplication(pkg)
109                         .getIdentifier(newName, "drawable", pkg)
110                 if (newId == 0) {
111                     Log.v(TAG, "oops, " + newName + " doesn't resolve from pkg " + pkg)
112                 } else if (newId != oldResId) {
113                     // got a good one
114                     resId = newId
115                     resName = newName
116                     break
117                 }
118             }
119         } catch (e: RuntimeException) {
120             Log.v(TAG, "problem loading puzzle, using fallback", e)
121         }
122         loadPuzzle()
123     }
124 
getPackageNameForResourceNamenull125     fun getPackageNameForResourceName(name: String): String {
126         return if (name.contains(":") && !name.startsWith("android:")) {
127             name.substring(0, name.indexOf(":"))
128         } else {
129             packageName
130         }
131     }
132 
checkVictorynull133     fun checkVictory() {
134         if (q.check()) {
135             val dp = resources.displayMetrics.density
136 
137             val label: Button = findViewById(R.id.label)
138             label.text = resName.replace(Regex("^.*/"), "")
139             val drawable = icon?.loadDrawable(this)?.also {
140                 it.setBounds(0, 0, (32 * dp).toInt(), (32 * dp).toInt())
141                 it.setTint(label.currentTextColor)
142             }
143             label.setCompoundDrawables(drawable, null, null, null)
144 
145             label.visibility = VISIBLE
146         } else {
147             label.visibility = GONE
148         }
149     }
150 
loadPuzzlenull151     fun loadPuzzle() {
152         Log.v(TAG, "loading " + resName + " at " + q.width + "x" + q.height)
153 
154         val dp = resources.displayMetrics.density
155 
156         icon = Icon.createWithResource(getPackageNameForResourceName(resName), resId)
157         q.load(this, icon!!)
158 
159         if (q.isBlank()) {
160             // this is a really boring puzzle, let's try again
161             resId = 0
162             resName = ""
163             recreate()
164             return
165         }
166 
167         grid.removeAllViews()
168         grid.columnCount = q.width + 1
169         grid.rowCount = q.height + 1
170 
171         label.visibility = GONE
172 
173         val orientation = resources.configuration.orientation
174 
175         // clean this up a bit
176         val minSide = resources.configuration.smallestScreenWidthDp - 25 // ish
177         val size = (minSide / (q.height + 0.5) * dp).toInt()
178 
179         val sb = StringBuffer()
180 
181         for (j in 0 until grid.rowCount) {
182             for (i in 0 until grid.columnCount) {
183                 val tv: View
184                 val params = GridLayout.LayoutParams().also {
185                     it.width = size
186                     it.height = size
187                     it.setMargins(1, 1, 1, 1)
188                     it.rowSpec = GridLayout.spec(GridLayout.UNDEFINED, GridLayout.TOP) // UGH
189                 }
190                 val x = i - 1
191                 val y = j - 1
192                 if (i > 0 && j > 0) {
193                     if (i == 1 && j > 1) sb.append("\n")
194                     sb.append(if (q.getDataAt(x, y) == 0) " " else "X")
195                     tv = PixelButton(this)
196                     tv.isChecked = q.getUserMark(x, y) != 0
197                     tv.setOnClickListener {
198                         q.setUserMark(x, y, if (tv.isChecked) 0xFF else 0)
199                         val columnCorrect = (grid.getChildAt(i) as? ClueView)?.check(q) ?: false
200                         val rowCorrect = (grid.getChildAt(j*(grid.columnCount)) as? ClueView)
201                                 ?.check(q) ?: false
202                         if (columnCorrect && rowCorrect) {
203                             checkVictory()
204                         } else {
205                             label.visibility = GONE
206                         }
207                     }
208                 } else if (i == j) { // 0,0
209                     tv = View(this)
210                     tv.visibility = GONE
211                 } else {
212                     tv = ClueView(this)
213                     if (j == 0) {
214                         tv.textRotation = 90f
215                         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
216                             params.height /= 2
217                             tv.showText = false
218                         } else {
219                             params.height = (96 * dp).toInt()
220                         }
221                         if (x >= 0) {
222                             tv.setColumn(q, x)
223                         }
224                     }
225                     if (i == 0) {
226                         if (orientation == Configuration.ORIENTATION_PORTRAIT) {
227                             params.width /= 2
228                             tv.showText = false
229                         } else {
230                             params.width = (96 * dp).toInt()
231                         }
232                         if (y >= 0) {
233                             tv.setRow(q, y)
234                         }
235                     }
236                 }
237                 grid.addView(tv, params)
238             }
239         }
240 
241         Log.v(TAG, "icon: \n" + sb)
242     }
243 }
244 
245 class PixelButton(context: Context) : CompoundButton(context) {
246     init {
247         setBackgroundResource(R.drawable.pixel_bg)
248         isClickable = true
249         isEnabled = true
250     }
251 }
252 
253 class ClueView(context: Context) : View(context) {
254     var row: Int = -1
255     var column: Int = -1
256     var textRotation: Float = 0f
257     var text: CharSequence = ""
258     var showText = true
259     val paint: TextPaint
260     val incorrectColor: Int
261     val correctColor: Int
262 
263     init {
264         setBackgroundColor(0)
<lambda>null265         paint = TextPaint().also {
266             it.textSize = 14f * context.resources.displayMetrics.density
267             it.color = context.getColor(R.color.q_clue_text)
268             it.typeface = Typeface.DEFAULT_BOLD
269             it.textAlign = Paint.Align.CENTER
270         }
271         incorrectColor = context.getColor(R.color.q_clue_bg)
272         correctColor = context.getColor(R.color.q_clue_bg_correct)
273     }
274 
setRownull275     fun setRow(q: Quare, row: Int): Boolean {
276         this.row = row
277         this.column = -1
278         this.textRotation = 0f
279         text = q.getRowClue(row).joinToString("-")
280         return check(q)
281     }
setColumnnull282     fun setColumn(q: Quare, column: Int): Boolean {
283         this.column = column
284         this.row = -1
285         this.textRotation = 90f
286         text = q.getColumnClue(column).joinToString("-")
287         return check(q)
288     }
checknull289     fun check(q: Quare): Boolean {
290         val correct = q.check(column, row)
291         setBackgroundColor(if (correct) correctColor else incorrectColor)
292         return correct
293     }
294 
onDrawnull295     override fun onDraw(canvas: Canvas?) {
296         super.onDraw(canvas)
297         if (!showText) return
298         canvas?.let {
299             val x = canvas.width / 2f
300             val y = canvas.height / 2f
301             var textWidth = canvas.width
302             if (textRotation != 0f) {
303                 canvas.rotate(textRotation, x, y)
304                 textWidth = canvas.height
305             }
306             val textLayout = StaticLayout.Builder.obtain(
307                     text, 0, text.length, paint, textWidth).build()
308             canvas.translate(x, y - textLayout.height / 2)
309             textLayout.draw(canvas)
310         }
311     }
312 }
313